From 9ea81df7100c6f0b4e34fcfd52780be16cb6b57a Mon Sep 17 00:00:00 2001 From: crgisch Date: Mon, 23 Sep 2024 12:37:09 -0300 Subject: [PATCH 1/2] feat: job image deploy --- internal/provider/provider.go | 5 +- .../provider/resource_tsuru_job_deploy.go | 175 ++++++++++++++++++ 2 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 internal/provider/resource_tsuru_job_deploy.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b7ac3d5..3936fa4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -70,8 +70,9 @@ func Provider() *schema.Provider { "tsuru_app_deploy": resourceTsuruApplicationDeploy(), "tsuru_app": resourceTsuruApplication(), - "tsuru_job": resourceTsuruJob(), - "tsuru_job_env": resourceTsuruJobEnvironment(), + "tsuru_job": resourceTsuruJob(), + "tsuru_job_env": resourceTsuruJobEnvironment(), + "tsuru_job_deploy": resourceTsuruJobDeploy(), "tsuru_router": resourceTsuruRouter(), "tsuru_plan": resourceTsuruPlan(), diff --git a/internal/provider/resource_tsuru_job_deploy.go b/internal/provider/resource_tsuru_job_deploy.go new file mode 100644 index 0000000..7418877 --- /dev/null +++ b/internal/provider/resource_tsuru_job_deploy.go @@ -0,0 +1,175 @@ +// Copyright 2023 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package provider + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceTsuruJobDeploy() *schema.Resource { + return &schema.Resource{ + Description: "Perform an job deploy. Currently, only supporting deploys via prebuilt container images; in order to deploy via tsuru platforms please use tsuru-client", + CreateContext: resourceTsuruJobDeployDo, + UpdateContext: resourceTsuruJobDeployDo, + ReadContext: resourceTsuruJobDeployRead, + DeleteContext: resourceTsuruJobDeployDelete, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + Schema: map[string]*schema.Schema{ + "job": { + Type: schema.TypeString, + Description: "Job name", + Required: true, + ForceNew: true, + }, + "image": { + Type: schema.TypeString, + Description: "Docker Image", + Required: true, + }, + "wait": { + Type: schema.TypeBool, + Description: "Wait for the rollout of deploy", + Optional: true, + Default: true, + }, + "status": { + Type: schema.TypeString, + Description: "After apply may be three kinds of statuses: running or failed or finished", + Computed: true, + }, + "output_image": { + Type: schema.TypeString, + Description: "Image generated after success of deploy", + Computed: true, + }, + }, + } +} + +func resourceTsuruJobDeployDo(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + provider := meta.(*tsuruProvider) + + if !d.HasChange("image") { + return nil + } + + job := d.Get("job").(string) + + values := url.Values{} + values.Set("origin", "image") + values.Set("image", d.Get("image").(string)) + values.Set("message", "deploy via terraform") + + var buf bytes.Buffer + buf.WriteString(values.Encode()) + + url := fmt.Sprintf("%s/1.23/jobs/%s/deploy", provider.Host, job) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) + if err != nil { + return diag.FromErr(err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + token := provider.Token + if token == "" { + token = deployToken() + } + req.Header.Set("Authorization", token) + + wait := d.Get("wait").(bool) + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + log.Println("[DEBUG] failed to request deploy", err) + return diag.FromErr(err) + } + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return diag.FromErr(err) + } + return diag.Errorf("Could not deploy, status code: %d, message: %s", resp.StatusCode, string(body)) + } + + eventID := resp.Header.Get("X-Tsuru-Eventid") + d.SetId(eventID) + + if wait { + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + log.Println("[DEBUG]", scanner.Text()) + } + + if err := scanner.Err(); err != nil { + log.Fatal("[ERROR]", err) + } + + err = waitForEventComplete(ctx, provider, eventID) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceTsuruJobDeployRead(ctx, d, meta) +} + +func resourceTsuruJobDeployRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + provider := meta.(*tsuruProvider) + + id := d.Id() + + e, _, err := provider.TsuruClient.EventApi.EventInfo(ctx, id) + + if err != nil { + return diag.FromErr(err) + } + + status := "" + if e.Running { + status = "running" + } else if e.Error != "" { + status = "error" + } else if !e.EndTime.IsZero() { + status = "finished" + } + + d.Set("status", status) + + data, err := decodeRawBSONMap(e.EndCustomData) + if err == nil { + image, found := data["image"] + if found { + d.Set("output_image", image.(string)) + } else { + d.Set("output_image", "") + } + } else { + log.Println("[ERROR] found error decoding endCustomData", err) + } + + return nil +} + +func resourceTsuruJobDeployDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Println("[DEBUG] delete a deploy is a no-op by terraform") + return nil +} From 61d37b1e4e3961adbf66bc8d9ecb14942c2bb581 Mon Sep 17 00:00:00 2001 From: crgisch Date: Mon, 23 Sep 2024 12:37:29 -0300 Subject: [PATCH 2/2] test: job image deploy --- .../resource_tsuru_job_deploy_test.go | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 internal/provider/resource_tsuru_job_deploy_test.go diff --git a/internal/provider/resource_tsuru_job_deploy_test.go b/internal/provider/resource_tsuru_job_deploy_test.go new file mode 100644 index 0000000..7b98feb --- /dev/null +++ b/internal/provider/resource_tsuru_job_deploy_test.go @@ -0,0 +1,139 @@ +// Copyright 2021 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package provider + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + echo "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestAccResourceTsuruJobDeploy(t *testing.T) { + fakeServer := echo.New() + + fakeServer.POST("/1.23/jobs/:job/deploy", func(c echo.Context) error { + c.Response().Header().Set("X-Tsuru-Eventid", "abc-123") + + formParams, err := c.FormParams() + if err != nil { + return err + } + assert.Equal(t, url.Values{ + "image": {"fake-repo/job:0.1.0"}, + "message": {"deploy via terraform"}, + "origin": {"image"}}, + formParams) + + return c.String(http.StatusOK, "OK") + }) + + iterationCount := 0 + fakeServer.GET("/1.1/events/:eventID", func(c echo.Context) error { + iterationCount++ + + return c.JSON(http.StatusOK, map[string]interface{}{ + "Running": iterationCount < 2, + "EndTime": "2023-01-04T19:26:20.946Z", + "EndCustomData": map[string]interface{}{ + "Kind": 3, + "Data": "GwAAAAJpbWFnZQALAAAAdGVzdDoxLjIuMwAA", + }, + }) + }) + + fakeServer.HTTPErrorHandler = func(err error, c echo.Context) { + t.Errorf("methods=%s, path=%s, err=%s", c.Request().Method, c.Path(), err.Error()) + } + server := httptest.NewServer(fakeServer) + os.Setenv("TSURU_TARGET", server.URL) + + resourceName := "tsuru_job_deploy.deploy" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccResourceTsuruJobDeploy_basic(server.URL), + Check: resource.ComposeAggregateTestCheckFunc( + testAccResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "job", "my-job"), + resource.TestCheckResourceAttr(resourceName, "image", "fake-repo/job:0.1.0"), + resource.TestCheckResourceAttr(resourceName, "status", "finished"), + resource.TestCheckResourceAttr(resourceName, "output_image", "test:1.2.3"), + ), + }, + }, + }) +} + +func TestAccResourceTsuruJobDeployFailed(t *testing.T) { + fakeServer := echo.New() + + fakeServer.POST("/1.23/jobs/:job/deploy", func(c echo.Context) error { + c.Response().Header().Set("X-Tsuru-Eventid", "abc-123") + + formParams, err := c.FormParams() + if err != nil { + return err + } + assert.Equal(t, url.Values{ + "image": {"fake-repo/job:0.1.0"}, + "message": {"deploy via terraform"}, + "origin": {"image"}}, + formParams) + + return c.String(http.StatusOK, "OK") + }) + + fakeServer.GET("/1.1/events/:eventID", func(c echo.Context) error { + + return c.JSON(http.StatusOK, map[string]interface{}{ + "Running": false, + "Error": "deploy failed", + "EndTime": "2023-01-04T19:26:20.946Z", + }) + }) + + fakeServer.HTTPErrorHandler = func(err error, c echo.Context) { + t.Errorf("methods=%s, path=%s, err=%s", c.Request().Method, c.Path(), err.Error()) + } + server := httptest.NewServer(fakeServer) + os.Setenv("TSURU_TARGET", server.URL) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccResourceTsuruJobDeploy_basic(server.URL), + ExpectError: regexp.MustCompile("deploy failed, see details of event ID: abc-123"), + }, + }, + }) +} + +func testAccResourceTsuruJobDeploy_basic(serverURL string) string { + return fmt.Sprintf(` + + provider "tsuru" { + host = "%s" + } + + resource "tsuru_job_deploy" "deploy" { + job = "my-job" + image = "fake-repo/job:0.1.0" + } +`, serverURL) +}