Skip to content

Commit

Permalink
Merge pull request #14 from crusoecloud/v1alpha5
Browse files Browse the repository at this point in the history
V1Alpha5 Support
  • Loading branch information
agutierrez8 authored Dec 6, 2023
2 parents f7a8531 + 97b95e8 commit ca014af
Show file tree
Hide file tree
Showing 23 changed files with 926 additions and 263 deletions.
13 changes: 13 additions & 0 deletions crusoe/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/crusoecloud/terraform-provider-crusoe/internal/firewall_rule"
"github.com/crusoecloud/terraform-provider-crusoe/internal/ib_network"
"github.com/crusoecloud/terraform-provider-crusoe/internal/ib_partition"
"github.com/crusoecloud/terraform-provider-crusoe/internal/project"
"github.com/crusoecloud/terraform-provider-crusoe/internal/vm"
)

Expand Down Expand Up @@ -51,6 +52,7 @@ func (p *crusoeProvider) DataSources(_ context.Context) []func() datasource.Data
vm.NewVMDataSource,
disk.NewDisksDataSource,
ib_network.NewIBNetworkDataSource,
project.NewProjectsDataSource,
}
}

Expand All @@ -61,6 +63,7 @@ func (p *crusoeProvider) Resources(_ context.Context) []func() resource.Resource
disk.NewDiskResource,
firewall_rule.NewFirewallRuleResource,
ib_partition.NewIBPartitionResource,
project.NewProjectResource,
}
}

Expand Down Expand Up @@ -102,6 +105,16 @@ func (p *crusoeProvider) Configure(ctx context.Context, req provider.ConfigureRe
)
}

if clientConfig.DefaultProject == "" {
resp.Diagnostics.AddAttributeWarning(
path.Root("default_project"),
"Missing Crusoe Default Project",
"The provider did not find a default project specified in the configuration file and will attempt to infer the project to use if not specified. "+
"Set the value in ~/.crusoe/config. "+
"If either is already set, ensure the value is not empty.",
)
}

if resp.Diagnostics.HasError() {
return
}
Expand Down
23 changes: 17 additions & 6 deletions examples/infiniband/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ terraform {
}
}


locals {
my_ssh_key = file("~/.ssh/id_ed25519.pub")
}
Expand All @@ -24,22 +25,32 @@ resource "crusoe_ib_partition" "my_partition" {
# above. alternatively, they can be obtain with the CLI by
# crusoe networking ib-network list
ib_network_id = "<ib_network_id>"
project_id = crusoe_project.my_project.id
}

# create two VMs, both in the same Infiniband partition
# create multiple VMs, all in the same Infiniband partition
resource "crusoe_compute_instance" "my_vm1" {
count = 8

name = "ib-vm-${count.index}"
type = "a100-80gb-sxm-ib.8x" # IB enabled VM type, `a100-80gb-sxm-ib.8x` or h100-80gb-sxm-ib.8x`
location = "us-east1-a" # IB currently only supported in `us-east1-a`
image = "ubuntu20.04-nvidia-sxm-docker:latest" # IB image, see full list at https://docs.crusoecloud.com/compute/images/overview/index.html#list-of-curated-images
ib_partition_id = crusoe_ib_partition.my_partition.id
type = "a100-80gb-sxm-ib.8x" # IB enabled VM type
location = "us-east1-a" # IB currently only supported at us-east1-a
image = "ubuntu22.04-nvidia-sxm-docker:latest" # IB image

ssh_key = local.my_ssh_key

host_channel_adapters = [
{
ib_partition_id = crusoe_ib_partition.my_partition.id
}
]
disks = [
// disk attached at startup
crusoe_storage_disk.data_disk
{
id = crusoe_storage_disk.data_disk.id
attachment_type = "data"
mode = "read-only"
}
]
}

Expand Down
58 changes: 58 additions & 0 deletions examples/project-variable/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
terraform {
required_providers {
crusoe = {
source = "registry.terraform.io/crusoecloud/crusoe"
}
}
}

locals {
my_ssh_key = file("~/.ssh/id_ed25519.pub")
}

variable "project_id" {
type = string
default = "<PROJECT_ID>"
}

// new VM
resource "crusoe_compute_instance" "my_vm" {
name = "my-new-vm"
type = "a40.1x"
location = "us-northcentral1-a"

disks = [
// disk attached at startup
{
id = crusoe_storage_disk.data_disk.id
mode = "read-write"
attachment_type = "data"
}
]

ssh_key = local.my_ssh_key
project_id = var.project_id

}

resource "crusoe_storage_disk" "data_disk" {
name = "data-disk"
size = "200GiB"
project_id = var.project_id
location = "us-northcentral1-a"
}

// firewall rule
// note: this allows all ingress over TCP to our VM
resource "crusoe_vpc_firewall_rule" "open_fw_rule" {
network = crusoe_compute_instance.my_vm.network_interfaces[0].network
name = "example-terraform-rule"
action = "allow"
direction = "ingress"
protocols = "tcp"
source = "0.0.0.0/0"
source_ports = "1-65535"
destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address
destination_ports = "1-65535"
project_id = var.project_id
}
21 changes: 12 additions & 9 deletions examples/vms/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ resource "crusoe_compute_instance" "my_vm" {
type = "a40.1x"
location = "us-northcentral1-a"

# optionally specify a different base image
#image = "nvidia-docker"

ssh_key = local.my_ssh_key
startup_script = file("startup.sh")
# specify the base image
image = "ubuntu20.04:latest"

disks = [
// attached at startup
crusoe_storage_disk.data_disk
]
// disk attached at startup
{
id = crusoe_storage_disk.data_disk.id
mode = "read-only"
attachment_type = "data"
}
]

ssh_key = local.my_ssh_key

}

// attached disk
resource "crusoe_storage_disk" "data_disk" {
name = "data-disk"
size = "200GiB"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.18
require (
github.com/BurntSushi/toml v0.3.1
github.com/antihax/optional v1.0.0
github.com/crusoecloud/client-go v0.1.22
github.com/crusoecloud/client-go v0.1.32
github.com/hashicorp/terraform-plugin-framework v1.3.5
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/crusoecloud/client-go v0.1.22 h1:SE077syXicRyq9/1B9V1+ocYJdGZZRr6pdkimKhUPQM=
github.com/crusoecloud/client-go v0.1.22/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY=
github.com/crusoecloud/client-go v0.1.32 h1:c1f0lo8/T/3dkdF5KbxjWSjP1fnG+DE0luOo6q1jKWY=
github.com/crusoecloud/client-go v0.1.32/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
4 changes: 3 additions & 1 deletion internal/common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
const (
configFilePath = "/.crusoe/config" // full path is this appended to the user's home path

defaultApiEndpoint = "https://api.crusoecloud.com/v1alpha4"
defaultApiEndpoint = "https://api.crusoecloud.com/v1alpha5"
)

// Config holds options that can be set via ~/.crusoe/config and env variables.
Expand All @@ -19,6 +19,7 @@ type Config struct {
SecretKey string `toml:"secret_key"`
SSHPublicKeyFile string `toml:"ssh_public_key_file"`
ApiEndpoint string `toml:"api_endpoint"`
DefaultProject string `toml:"default_project"`
}

// ConfigFile reflects the structure of a valid Crusoe config, which should have a default profile at the root level.
Expand Down Expand Up @@ -48,6 +49,7 @@ func GetConfig() (*Config, error) {
config.AccessKeyID = configFile.Default.AccessKeyID
config.SecretKey = configFile.Default.SecretKey
config.SSHPublicKeyFile = configFile.Default.SSHPublicKeyFile
config.DefaultProject = configFile.Default.DefaultProject

if configFile.Default.ApiEndpoint != "" {
config.ApiEndpoint = configFile.Default.ApiEndpoint
Expand Down
106 changes: 62 additions & 44 deletions internal/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/antihax/optional"

Check failure on line 8 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard,default,prefix(github.com/crusoecloud) (gci)
"github.com/hashicorp/terraform-plugin-framework/diag"
"net/http"
"strings"
"time"

Check failure on line 12 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofumpt`-ed (gofumpt)

Check failure on line 13 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard,default,prefix(github.com/crusoecloud) (gci)
"github.com/antihax/optional"

swagger "github.com/crusoecloud/client-go/swagger/v1alpha4"
swagger "github.com/crusoecloud/client-go/swagger/v1alpha5"
)

const (
// TODO: pull from config set during build
version = "v0.4.1"
version = "v0.5.0"

pollInterval = 2 * time.Second

Expand All @@ -36,14 +36,11 @@ var (
OpInProgress opStatus = "IN_PROGRESS"
OpFailed opStatus = "FAILED"

errNoOperations = errors.New("no operation with id found")
errUnableToGetOpRes = errors.New("failed to get result of operation")

errAmbiguousRole = errors.New("user is associated with multiple roles - please contact [email protected]")
errNoRoleAssociation = errors.New("user is not associated with any role")

// fallback error presented to the user in unexpected situations
errUnexpected = errors.New("An unexpected error occurred. Please try again, and if the problem persists, contact [email protected].")
errMultipleProjects = errors.New("User has multiple projects. Please specify a project to be used.")
errUnexpected = errors.New("An unexpected error occurred. Please try again, and if the problem persists, contact [email protected].")
)

// NewAPIClient initializes a new Crusoe API client with the given configuration.
Expand All @@ -60,46 +57,19 @@ func NewAPIClient(host, key, secret string) *swagger.APIClient {
return swagger.NewAPIClient(cfg)
}

// GetRole creates a get Role request and calls the API.
// This function returns a role id if the user's role can be determined
// (i.e. user only has one role, which is the case for v0).
func GetRole(ctx context.Context, api *swagger.APIClient) (string, error) {
opts := swagger.RolesApiGetRolesOpts{
OrgId: optional.EmptyString(),
}

resp, httpResp, err := api.RolesApi.GetRoles(ctx, &opts)
if err != nil {
return "", fmt.Errorf("could not get roles: %w", err)
}
defer httpResp.Body.Close()

switch len(resp.Roles) {
case 0:
return "", errNoRoleAssociation
case 1:
return resp.Roles[0].Id, nil
default:
// user has multiple roles: unable to disambiguate
return "", errAmbiguousRole
}
}

// AwaitOperation polls an async API operation until it resolves into a success or failure state.
func AwaitOperation(ctx context.Context, op *swagger.Operation,
getFunc func(context.Context, string) (swagger.ListOperationsResponseV1Alpha4, *http.Response, error)) (
func AwaitOperation(ctx context.Context, op *swagger.Operation, projectID string,
getFunc func(context.Context, string, string) (swagger.Operation, *http.Response, error)) (
*swagger.Operation, error,
) {
for op.State == string(OpInProgress) {
updatedOps, httpResp, err := getFunc(ctx, op.OperationId)
updatedOps, httpResp, err := getFunc(ctx, projectID, op.OperationId)
if err != nil {
return nil, fmt.Errorf("error getting operation with id %s: %w", op.OperationId, err)
}
httpResp.Body.Close()
if len(updatedOps.Operations) == 0 {
return nil, errNoOperations
}
op = &updatedOps.Operations[0]

op = &updatedOps

time.Sleep(pollInterval)
}
Expand All @@ -122,10 +92,10 @@ func AwaitOperation(ctx context.Context, op *swagger.Operation,

// AwaitOperationAndResolve awaits an async API operation and attempts to parse the response as an instance of T,
// if the operation was successful.
func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation,
getFunc func(context.Context, string) (swagger.ListOperationsResponseV1Alpha4, *http.Response, error),
func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation, projectID string,
getFunc func(context.Context, string, string) (swagger.Operation, *http.Response, error),
) (*T, *swagger.Operation, error) {
op, err := AwaitOperation(ctx, op, getFunc)
op, err := AwaitOperation(ctx, op, projectID, getFunc)
if err != nil {
return nil, op, err
}
Expand All @@ -138,6 +108,54 @@ func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation,
return result, op, nil
}

// GetFallbackProject queries the API to get the list of projects belonging to the
// logged in user. If there is one project belonging to the user, it returns that project
// else it adds an error to the diagnostics and returns.
func GetFallbackProject(ctx context.Context, client *swagger.APIClient, diag *diag.Diagnostics) (string, error) {

Check failure on line 114 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

importShadow: shadow of imported from 'github.com/hashicorp/terraform-plugin-framework/diag' package 'diag' (gocritic)

Check failure on line 115 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofumpt`-ed (gofumpt)
config, err := GetConfig()
if err != nil {
return "", fmt.Errorf("failed to get config: %v", err)
}

var opts = &swagger.ProjectsApiListProjectsOpts{

Check failure on line 121 in internal/common/util.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofumpt`-ed (gofumpt)
OrgId: optional.EmptyString(),
}

if config.DefaultProject != "" {
opts.ProjectName = optional.NewString(config.DefaultProject)
}

dataResp, httpResp, err := client.ProjectsApi.ListProjects(ctx, opts)

defer httpResp.Body.Close()

if err != nil {
diag.AddError("Failed to retrieve project ID",
"Failed to retrieve project ID for the authenticated user.")

return "", err
}

if len(dataResp.Items) != 1 {
diag.AddError("Multiple projects found.",
"Multiple projects found for the authenticated user. Unable to determine which project to use.")

return "", errMultipleProjects
}

projectID := dataResp.Items[0].Id

if config.DefaultProject == "" {
diag.AddWarning("Default project not specified",
fmt.Sprintf("A project_id was not specified in the configuration file. "+
"Please specify a project in the terraform file or set a 'default_project' in your configuration file. "+
"Falling back to project: %s.", dataResp.Items[0].Name))
}

return projectID, nil
}

func parseOpResult[T any](opResult interface{}) (*T, error) {
b, err := json.Marshal(opResult)
if err != nil {
Expand Down
Loading

0 comments on commit ca014af

Please sign in to comment.