Skip to content

Commit

Permalink
feat: job deploy image (#59)
Browse files Browse the repository at this point in the history
* feat: job image deploy

* test: job image deploy
  • Loading branch information
crgisch authored Oct 15, 2024
1 parent cc4d422 commit f90fb2c
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 2 deletions.
5 changes: 3 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
175 changes: 175 additions & 0 deletions internal/provider/resource_tsuru_job_deploy.go
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 139 additions & 0 deletions internal/provider/resource_tsuru_job_deploy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit f90fb2c

Please sign in to comment.