diff --git a/docs/resources/apple_silicon_runner.md b/docs/resources/apple_silicon_runner.md new file mode 100644 index 0000000000..058117e2b1 --- /dev/null +++ b/docs/resources/apple_silicon_runner.md @@ -0,0 +1,43 @@ +--- +subcategory: "Apple Silicon" +page_title: "Scaleway: scaleway_apple_silicon_runner" +--- + +# Resource: scaleway_apple_silicon_runner + +Creates and manages Scaleway Apple silicon runners. + +## Example Usage + +```terraform +resource "scaleway_apple_silicon_runner" "main" { + name = "my-github-runner" + ci_provider = "github" + url = "https://github.com/my-org/my-repo" + token = "my-token" +} +``` + +## Argument Reference + +- `ci_provider` - (Required) The CI/CD provider for the runner. Must be either 'github' or 'gitlab' +- `token` - (Required) The token used to authenticate the runner to run +- `url` - (Required) The URL of the runner to run +- `name` - (Optional) The name of the runner +- `project_id` - (Optional) The project_id you want to attach the resource to +- `zone` - (Optional) The zone of the runner + +## Attributes Reference + +- `id` - The ID of the runner. +- `status` - The status of the runner +- `labels` - A list of labels applied to the runner. Only for github provider +- `error_message` - The error message if the runner is in error state + +## Import + +Runner can be imported using the `{zone}/{id}`, e.g. + +```bash +terraform import scaleway_apple_silicon_runner.main fr-par-1/11111111-1111-1111-1111-111111111111 +``` diff --git a/internal/services/applesilicon/runner.go b/internal/services/applesilicon/runner.go new file mode 100644 index 0000000000..e4f91faed1 --- /dev/null +++ b/internal/services/applesilicon/runner.go @@ -0,0 +1,220 @@ +package applesilicon + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + applesilicon "github.com/scaleway/scaleway-sdk-go/api/applesilicon/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" +) + +func ResourceRunner() *schema.Resource { + return &schema.Resource{ + CreateContext: ResourceAppleSiliconRunnerCreate, + ReadContext: ResourceAppleSiliconRunnerRead, + UpdateContext: ResourceAppleSiliconRunnerUpdate, + DeleteContext: ResourceAppleSiliconRunnerDelete, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Default: schema.DefaultTimeout(5 * time.Minute), + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "The name of the runner", + Computed: true, + Optional: true, + }, + "ci_provider": { + Type: schema.TypeString, + Required: true, + Description: "The CI/CD provider for the runner. Must be either 'github' or 'gitlab'", + ValidateDiagFunc: verify.ValidateEnum[applesilicon.RunnerConfigurationProvider](), + }, + "url": { + Type: schema.TypeString, + Required: true, + Description: "The URL of the runner to run", + }, + "token": { + Type: schema.TypeString, + Sensitive: true, + Required: true, + Description: "The token used to authenticate the runner to run", + }, + "labels": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "A list of labels that should be applied to the runner.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The status of the runner", + }, + "error_message": { + Type: schema.TypeString, + Computed: true, + Description: "The error message of the runner", + }, + "zone": zonal.Schema(), + "project_id": account.ProjectIDSchema(), + }, + } +} + +func ResourceAppleSiliconRunnerCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + asAPI, zone, err := newAPIWithZone(d, m) + if err != nil { + return diag.FromErr(err) + } + + provider := d.Get("ci_provider").(string) + + runnerConfig := &applesilicon.RunnerConfigurationV2{ + Name: d.Get("name").(string), + Provider: applesilicon.RunnerConfigurationV2Provider(provider), + GithubConfiguration: nil, + GitlabConfiguration: nil, + } + + if provider == "github" { + runnerConfig.GithubConfiguration = &applesilicon.GithubRunnerConfiguration{ + URL: d.Get("url").(string), + Token: d.Get("token").(string), + Labels: nil, + } + } + + if provider == "gitlab" { + runnerConfig.GitlabConfiguration = &applesilicon.GitlabRunnerConfiguration{ + URL: d.Get("url").(string), + Token: d.Get("token").(string), + } + } + + createRunnerReq := &applesilicon.CreateRunnerRequest{ + Zone: zone, + ProjectID: d.Get("project_id").(string), + RunnerConfiguration: runnerConfig, + } + + runner, err := asAPI.CreateRunner(createRunnerReq, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(zonal.NewIDString(zone, runner.ID)) + + return ResourceAppleSiliconRunnerRead(ctx, d, m) +} + +func ResourceAppleSiliconRunnerRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + asAPI, zone, ID, err := NewAPIWithZoneAndID(m, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + runner, err := asAPI.GetRunner(&applesilicon.GetRunnerRequest{ + Zone: zone, + RunnerID: ID, + }, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + d.SetId("") + + return nil + } + return diag.FromErr(err) + } + + _ = d.Set("name", runner.Configuration.Name) + _ = d.Set("ci_provider", runner.Configuration.Provider) + _ = d.Set("status", runner.Status) + _ = d.Set("error_message", runner.ErrorMessage) + + if runner.Configuration.Provider == "github" { + _ = d.Set("url", runner.Configuration.GithubConfiguration.URL) + _ = d.Set("labels", runner.Configuration.GithubConfiguration.Labels) + } + + if runner.Configuration.Provider == "gitlab" { + _ = d.Set("url", runner.Configuration.GitlabConfiguration.URL) + } + + return nil +} + +func ResourceAppleSiliconRunnerUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + asAPI, zone, ID, err := NewAPIWithZoneAndID(m, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + provider := d.Get("ci_provider").(string) + + runnerConfig := &applesilicon.RunnerConfigurationV2{ + Name: d.Get("name").(string), + Provider: applesilicon.RunnerConfigurationV2Provider(provider), + GithubConfiguration: nil, + GitlabConfiguration: nil, + } + + if provider == "github" { + runnerConfig.GithubConfiguration = &applesilicon.GithubRunnerConfiguration{ + URL: d.Get("url").(string), + Token: d.Get("token").(string), + Labels: types.ExpandStrings(d.Get("labels")), + } + } + + if provider == "gitlab" { + runnerConfig.GitlabConfiguration = &applesilicon.GitlabRunnerConfiguration{ + URL: d.Get("url").(string), + Token: d.Get("token").(string), + } + } + + updateRunnerReq := &applesilicon.UpdateRunnerRequest{ + Zone: zone, + RunnerID: ID, + RunnerConfiguration: runnerConfig, + } + + _, err = asAPI.UpdateRunner(updateRunnerReq, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return ResourceAppleSiliconRunnerRead(ctx, d, m) +} + +func ResourceAppleSiliconRunnerDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + asAPI, zone, ID, err := NewAPIWithZoneAndID(m, d.Id()) + if err != nil { + return diag.FromErr(err) + } + runnerDeleteReq := &applesilicon.DeleteRunnerRequest{ + Zone: zone, + RunnerID: ID, + } + err = asAPI.DeleteRunner(runnerDeleteReq, scw.WithContext(ctx)) + if err != nil && !httperrors.Is403(err) { + return diag.FromErr(err) + } + return nil +} diff --git a/internal/services/applesilicon/runner_test.go b/internal/services/applesilicon/runner_test.go new file mode 100644 index 0000000000..50af0efb21 --- /dev/null +++ b/internal/services/applesilicon/runner_test.go @@ -0,0 +1,113 @@ +package applesilicon_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + applesiliconSDK "github.com/scaleway/scaleway-sdk-go/api/applesilicon/v1alpha1" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/applesilicon" +) + +func TestAccRunner_BasicGithub(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + t.Skip("can not register this cassette for security issue") + var githubUrl = os.Getenv("GITHUB_URL_AS") + var githubToken = os.Getenv("GITHUB_TOKEN_AS") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: isRunnerDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_apple_silicon_runner" "main" { + name = "TestAccRunnerGithub" + ci_provider = "github" + url = "%s" + token = "%s" + } + `, githubUrl, githubToken), + Check: resource.ComposeTestCheckFunc( + isRunnerPresent(tt, "scaleway_apple_silicon_runner.main"), + resource.TestCheckResourceAttr("scaleway_apple_silicon_runner.main", "name", "TestAccRunnerGithub"), + resource.TestCheckResourceAttr("scaleway_apple_silicon_runner.main", "ci_provider", "github"), + resource.TestCheckResourceAttr("scaleway_apple_silicon_runner.main", "url", githubUrl), + + // Computed + resource.TestCheckResourceAttrSet("scaleway_apple_silicon_runner.main", "status"), + ), + }, + { + Config: fmt.Sprintf(` + resource "scaleway_apple_silicon_runner" "main" { + name = "TestAccRunnerGithubUpdated" + ci_provider = "github" + url = "%s" + token = "%s" + } + `, githubUrl, githubToken), + Check: resource.ComposeTestCheckFunc( + isRunnerPresent(tt, "scaleway_apple_silicon_runner.main"), + resource.TestCheckResourceAttr("scaleway_apple_silicon_runner.main", "name", "TestAccRunnerGithubUpdated"), + ), + }, + }, + }) +} + +func isRunnerPresent(tt *acctest.TestTools, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + api, zone, id, err := applesilicon.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) + if err != nil { + return err + } + + _, err = api.GetRunner(&applesiliconSDK.GetRunnerRequest{ + Zone: zone, + RunnerID: id, + }) + + return err + } +} + +func isRunnerDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway_apple_silicon_runner" { + continue + } + + api, zone, id, err := applesilicon.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) + if err != nil { + return err + } + + _, err = api.GetRunner(&applesiliconSDK.GetRunnerRequest{ + Zone: zone, + RunnerID: id, + }) + + if err == nil { + return fmt.Errorf("runner still exists: %s", rs.Primary.ID) + } + + if !httperrors.Is403(err) { + return fmt.Errorf("unexpected error: %s", err) + } + } + + return nil + } +} diff --git a/internal/services/applesilicon/server.go b/internal/services/applesilicon/server.go index b2c4e3edd3..6dca65c042 100644 --- a/internal/services/applesilicon/server.go +++ b/internal/services/applesilicon/server.go @@ -61,6 +61,16 @@ func ResourceServer() *schema.Resource { Description: "The commitment period of the server", ValidateDiagFunc: verify.ValidateEnum[applesilicon.CommitmentType](), }, + "runner_ids": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: verify.IsUUIDorUUIDWithLocality(), + }, + Description: "List of runner ids attach to the server", + }, "public_bandwidth": { Type: schema.TypeInt, Optional: true, @@ -207,6 +217,10 @@ func ResourceAppleSiliconServerCreate(ctx context.Context, d *schema.ResourceDat Zone: zone, } + if runnerIDs, ok := d.GetOk("runner_ids"); ok { + createReq.AppliedRunnerConfigurations.RunnerConfigurationIDs = runnerIDs.([]string) + } + if bandwidth, ok := d.GetOk("public_bandwidth"); ok { createReq.PublicBandwidthBps = *types.ExpandUint64Ptr(bandwidth) } @@ -283,6 +297,7 @@ func ResourceAppleSiliconServerRead(ctx context.Context, d *schema.ResourceData, _ = d.Set("username", res.SSHUsername) _ = d.Set("public_bandwidth", int(res.PublicBandwidthBps)) _ = d.Set("zone", res.Zone) + _ = d.Set("runner_ids", res.AppliedRunnerConfigurationIDs) listPrivateNetworks, err := privateNetworkAPI.ListServerPrivateNetworks(&applesilicon.PrivateNetworkAPIListServerPrivateNetworksRequest{ Zone: res.Zone, @@ -385,6 +400,10 @@ func ResourceAppleSiliconServerUpdate(ctx context.Context, d *schema.ResourceDat req.PublicBandwidthBps = publicBandwidth } + if d.HasChange("runner_ids") { + req.AppliedRunnerConfigurations.RunnerConfigurationIDs = d.Get("runner_ids").([]string) + } + _, err = asAPI.UpdateServer(req, scw.WithContext(ctx)) if err != nil { return diag.FromErr(err) diff --git a/internal/services/applesilicon/testdata/runner-basic-github.cassette.yaml b/internal/services/applesilicon/testdata/runner-basic-github.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/internal/services/applesilicon/testdata/runner-basic-github.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/provider/sdkv2.go b/provider/sdkv2.go index 66b2cc9307..a2b6c12a13 100644 --- a/provider/sdkv2.go +++ b/provider/sdkv2.go @@ -130,6 +130,7 @@ func SDKProvider(config *Config) plugin.ProviderFunc { "scaleway_account_project": account.ResourceProject(), "scaleway_account_ssh_key": iam.ResourceSSKKey(), "scaleway_apple_silicon_server": applesilicon.ResourceServer(), + "scaleway_apple_silicon_runner": applesilicon.ResourceRunner(), "scaleway_autoscaling_instance_group": autoscaling.ResourceInstanceGroup(), "scaleway_autoscaling_instance_policy": autoscaling.ResourceInstancePolicy(), "scaleway_autoscaling_instance_template": autoscaling.ResourceInstanceTemplate(), diff --git a/templates/resources/apple_silicon_runner.md.tmpl b/templates/resources/apple_silicon_runner.md.tmpl new file mode 100644 index 0000000000..add24169da --- /dev/null +++ b/templates/resources/apple_silicon_runner.md.tmpl @@ -0,0 +1,44 @@ +{{- /*gotype: github.com/hashicorp/terraform-plugin-docs/internal/provider.ResourceTemplateType */ -}} +--- +subcategory: "Apple Silicon" +page_title: "Scaleway: scaleway_apple_silicon_runner" +--- + +# Resource: scaleway_apple_silicon_runner + +Creates and manages Scaleway Apple silicon runners. + +## Example Usage + +```terraform +resource "scaleway_apple_silicon_runner" "main" { + name = "my-github-runner" + ci_provider = "github" + url = "https://github.com/my-org/my-repo" + token = "my-token" +} +``` + +## Argument Reference + +- `ci_provider` - (Required) The CI/CD provider for the runner. Must be either 'github' or 'gitlab' +- `token` - (Required) The token used to authenticate the runner to run +- `url` - (Required) The URL of the runner to run +- `name` - (Optional) The name of the runner +- `project_id` - (Optional) The project_id you want to attach the resource to +- `zone` - (Optional) The zone of the runner + +## Attributes Reference + +- `id` - The ID of the runner. +- `status` - The status of the runner +- `labels` - A list of labels applied to the runner. Only for github provider +- `error_message` - The error message if the runner is in error state + +## Import + +Runner can be imported using the `{zone}/{id}`, e.g. + +```bash +terraform import scaleway_apple_silicon_runner.main fr-par-1/11111111-1111-1111-1111-111111111111 +```