Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider/stack: add new stack resource for Stacks API integration #155

Merged
merged 1 commit into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/internal/provider/scylla/ @rjeczalik
/internal/provider/stack/ @rjeczalik
/internal/provider/allowlistrule/ @rjeczalik
/internal/provider/cluster/ @rjeczalik @charconstpointer @ksinica
/internal/provider/cqlauth/ @rjeczalik
Expand Down
31 changes: 11 additions & 20 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ package provider

import (
"context"
"net/url"
"os"
"runtime"
"strconv"

"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/allowlistrule"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/cluster"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/connection"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/cqlauth"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/serverless"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/stack"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/provider/vpcpeering"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/tfcontext"

"github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla"

"github.com/hashicorp/go-cty/cty"
Expand All @@ -31,6 +30,12 @@ func envEndpoint() string {
return os.Getenv("SCYLLADB_CLOUD_ENDPOINT")
}

func ignoreMeta() bool {
s := os.Getenv("SCYLLADB_CLOUD_IGNORE_META")
ok, _ := strconv.ParseBool(s)
return ok
}

func New(context.Context) (*schema.Provider, error) {
p := &schema.Provider{
Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -70,6 +75,7 @@ func New(context.Context) (*schema.Provider, error) {
"scylladbcloud_vpc_peering": vpcpeering.ResourceVPCPeering(),
"scylladbcloud_serverless_cluster": serverless.ResourceServerlessCluster(),
"scylladbcloud_cluster_connection": connection.ResourceClusterConnection(),
"scylladbcloud_stack": stack.ResourceStack(),
},
}

Expand All @@ -84,29 +90,14 @@ func configure(ctx context.Context, p *schema.Provider, d *schema.ResourceData)
var (
endpoint = d.Get("endpoint").(string)
token = d.Get("token").(string)
ignore = ignoreMeta()
)

c, err := scylla.NewClient()
c, err := scylla.NewClient(endpoint, token, userAgent(p.TerraformVersion), ignore)
if err != nil {
return nil, diag.Errorf("could not create new Scylla client: %s", err)
}

ctx = tfcontext.AddProviderInfo(ctx, endpoint)
if c.Endpoint, err = url.Parse(endpoint); err != nil {
return nil, diag.FromErr(err)
}

if c.Meta, err = scylla.BuildCloudmeta(ctx, c); err != nil {
return nil, diag.Errorf("could not build Cloudmeta: %s", err)
}

c.Headers.Set("Accept", "application/json; charset=utf-8")
c.Headers.Set("User-Agent", userAgent(p.TerraformVersion))

if err := c.Auth(ctx, token); err != nil {
return nil, diag.FromErr(err)
}

return c, nil
}

Expand Down
128 changes: 128 additions & 0 deletions internal/provider/stack/stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package stack

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla/model"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
stackTimeout = 60 * time.Second
)

func ResourceStack() *schema.Resource {
return &schema.Resource{
CreateContext: resourceStackCreate,
ReadContext: resourceStackRead,
UpdateContext: resourceStackUpdate,
DeleteContext: resourceStackDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(stackTimeout),
Update: schema.DefaultTimeout(stackTimeout),
Delete: schema.DefaultTimeout(stackTimeout),
},

Schema: map[string]*schema.Schema{
"attributes": {
Description: "List of managed resources",
Required: true,
Type: schema.TypeMap,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func resourceStackCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var (
c = meta.(*scylla.Client)
s = &model.Stack{
RequestType: "Create",
ResourceProperties: d.Get("attributes").(map[string]interface{}),
}
)

id, err := sendStack(ctx, c, s)
if err != nil {
return diag.Errorf("failed to create stack: %s", err)
}

d.SetId(id)

return nil
}

func resourceStackRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}

func resourceStackUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var (
c = meta.(*scylla.Client)
s = &model.Stack{
RequestType: "Update",
ResourceProperties: d.Get("attributes").(map[string]interface{}),
}
)

_, err := sendStack(ctx, c, s)
if err != nil {
return diag.Errorf("failed to update stack: %s", err)
}

return nil
}

func resourceStackDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var (
c = meta.(*scylla.Client)
s = &model.Stack{
RequestType: "Delete",
ResourceProperties: d.Get("attributes").(map[string]interface{}),
}
)

_, err := sendStack(ctx, c, s)
if err != nil {
return diag.Errorf("failed to delete stack: %s", err)
}

return nil
}

func sendStack(ctx context.Context, c *scylla.Client, s *model.Stack) (string, error) {
auth := strings.Split(c.Token, ":")

if len(auth) != 2 {
return "", errors.New("invalid token format")
}

req := c.V2.Request(ctx, "POST", s, "/")

req.Header.Set("X-Scylla-Cloud-Stack-Flavor", "tf")

if err := c.V2.BasicSign(req, auth[0], []byte(auth[1])); err != nil {
return "", fmt.Errorf("failed to sign request: %w", err)
}

if _, err := c.V2.Do(req, s); err != nil {
return "", fmt.Errorf("failed to create stack: %w", err)
}

return auth[0], nil
}
70 changes: 41 additions & 29 deletions internal/scylla/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
stdpath "path"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"
v2scylla "github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla/v2"
"github.com/scylladb/terraform-provider-scylladbcloud/internal/tfcontext"

"github.com/eapache/go-resiliency/retrier"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

const (
Expand All @@ -34,6 +35,9 @@ type Client struct {
// API endpoint
Endpoint *url.URL

// Token is the API token used to authenticate requests.
Token string

// ErrCodes provides a human-readable translation of ScyllaDB API errors.
ErrCodes map[string]string // code -> message

Expand All @@ -44,52 +48,60 @@ type Client struct {
// AccountID holds the account ID used in requests to the API.
AccountID int64

Retry *retrier.Retrier // Retrier is used to retry requests to the API.
// Retry is used to retry requests to the API.
Retry *retrier.Retrier

// V2 is the client to call the V2 API, it does not require costly
// metadata building.
V2 *v2scylla.Client
}

func NewClient() (*Client, error) {
func NewClient(endpoint, token, useragent string, ignoreMeta bool) (*Client, error) {
errCodes, err := parse(codes, codesDelim, codesFunc)
if err != nil {
return nil, fmt.Errorf("failed to parse error codes: %w", err)
}

end, err := url.Parse(endpoint)
if err != nil {
return nil, err
}

ctx := context.Background()
retry := retrier.New(
retrier.ExponentialBackoff(5, 5*time.Second),
DefaultClassifier,
)

c := &Client{
Token: token,
ErrCodes: errCodes,
Headers: make(http.Header),
HTTPClient: http.DefaultClient,
Retry: retrier.New(
retrier.ExponentialBackoff(5, 5*time.Second),
DefaultClassifier,
HTTPClient: &http.Client{Timeout: defaultTimeout},
Retry: retry,
Endpoint: end,
V2: v2scylla.New(
v2scylla.WithRetryPolicy(retry),
v2scylla.WithUserAgent(useragent),
v2scylla.WithBaseURL(endpoint),
v2scylla.WithGlobalCookieJar(),
),
}

return c, nil
}

// NewClient represents a new client to call the API
func (c *Client) Auth(ctx context.Context, token string) error {
if c.HTTPClient == nil {
c.HTTPClient = &http.Client{Timeout: defaultTimeout}
}

if c.Headers == nil {
c.Headers = make(http.Header)
}

c.Headers.Set("Authorization", "Bearer "+token)
c.Headers.Set("Authorization", "Bearer "+c.Token)
c.Headers.Set("Accept", "application/json; charset=utf-8")
c.Headers.Set("User-Agent", useragent)

if c.Meta == nil {
var err error
if !ignoreMeta {
if c.Meta, err = BuildCloudmeta(ctx, c); err != nil {
return fmt.Errorf("error building metadata: %w", err)
return nil, fmt.Errorf("error building metadata: %w", err)
}
if err = c.findAndSaveAccountID(ctx); err != nil {
return nil, err
}
}

if err := c.findAndSaveAccountID(ctx); err != nil {
return err
}

return nil
return c, nil
}

func (c *Client) newHttpRequest(ctx context.Context, method, path string, reqBody interface{}, query ...string) (*http.Request, error) {
Expand Down
6 changes: 6 additions & 0 deletions internal/scylla/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,9 @@ func NodesDNSNames(n []Node) []string {
type Datacenters struct {
Datacenters []Datacenter `json:"dataCenters"`
}

type Stack struct {
RequestType string `json:"RequestType"`
RequestID string `json:"RequestId"`
ResourceProperties map[string]any `json:"ResourceProperties"`
}
Loading
Loading