diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cce3af2a6..25738645c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Release (2026-MM-DD) +- `functions`: + - [v0.1.0](services/functions/CHANGELOG.md#v010) + - `v1alphaapi`: + - **Feature**: initial release - `telemetrylink`: - [v0.6.0](services/telemetrylink/CHANGELOG.md#v060) - `v1api`: @@ -4840,4 +4844,4 @@ List of modules: - `ske`: [v0.7.0](services/ske/CHANGELOG.md#v070-2023-12-05) - Manage your STACKIT Kubernetes Engine resources: `Project`, `Cluster`, `Credentials`, `Options` - Waiters for async operations: `CreateOrUpdateClusterWaitHandler`, `DeleteClusterWaitHandler`, `CreateProjectWaitHandler`, `DeleteProjectWaitHandler`, `RotateCredentialsWaitHandler` - - [Usage example](https://github.com/stackitcloud/stackit-sdk-go/tree/main/examples/ske) + - [Usage example](https://github.com/stackitcloud/stackit-sdk-go/tree/main/examples/ske) \ No newline at end of file diff --git a/examples/functions/functions.go b/examples/functions/functions.go new file mode 100644 index 0000000000..26cdba1045 --- /dev/null +++ b/examples/functions/functions.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "fmt" + "os" + + functions "github.com/stackitcloud/stackit-sdk-go/services/functions/v1alphaapi" + wait "github.com/stackitcloud/stackit-sdk-go/services/functions/v1alphaapi/wait" +) + +func main() { + projectId := "PROJECT_ID" // the uuid of your STACKIT project + + // Create a new API client, that uses default authentication and configuration + functionsClient, err := functions.NewAPIClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Creating API client: %v\n", err) + os.Exit(1) + } + + // List functions + functionsList, err := functionsClient.DefaultAPI.ListFunctions(context.Background(), projectId).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error listing functions: %v\n", err) + os.Exit(1) + } + _, err = fmt.Fprintf(os.Stdout, "Function count: %d\n", len(functionsList.Functions)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when printing out function count: %v\n", err) + os.Exit(1) + } + + // List pull-credentials + pull_credentials, err := functionsClient.DefaultAPI.ListPullCredentials(context.Background(), projectId).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when listing pull-credentials count: %v\n", err) + os.Exit(1) + } + _, err = fmt.Fprintf(os.Stdout, "Pull-credentials count: %d\n", len(pull_credentials.PullCredentials)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when printing out pull-credentials count: %v\n", err) + os.Exit(1) + } + + // Create pull-credential + payload := *functions.NewCreatePullCredentialPayload("ghcr.io", "MY_TOKEN", "MY_USERNAME") + pull_credential, err := functionsClient.DefaultAPI.CreatePullCredential(context.Background(), projectId).CreatePullCredentialPayload(payload).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "erorr creating pull-credential: %v\n", err) + os.Exit(1) + } + _, err = fmt.Fprintf(os.Stdout, "Created Pull-credentials address: %d\n", len(pull_credential.Address)) + if err != nil { + fmt.Fprintf(os.Stderr, "can not print out function pull-credentials address: %v\n", err) + os.Exit(1) + } + + // List pull-credentials + pull_credentials, err = functionsClient.DefaultAPI.ListPullCredentials(context.Background(), projectId).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `ListPullCredentials`: %v\n", err) + os.Exit(1) + } + _, err = fmt.Fprintf(os.Stdout, "Pull-credentials count: %d\n", len(pull_credentials.PullCredentials)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when printing out pull-credentials count: %v\n", err) + os.Exit(1) + } + + // Create function deployment + functionData := *functions.NewCreateFunctionPayload("FUNKY_FUNC") + function, err := functionsClient.DefaultAPI.CreateFunction(context.Background(), projectId).CreateFunctionPayload(functionData).Execute() + if err != nil || function.Id == nil { + fmt.Fprintf(os.Stderr, "Error when calling `CreateFunction`: %v\n", err) + os.Exit(1) + } + _, err = fmt.Printf("Function with id %s created\n", *function.Id) + if err != nil { + fmt.Fprintf(os.Stderr, "Error printing out function: %v\n", err) + os.Exit(1) + } + + // Create function revision deployment + revisionData := *functions.NewCreateFunctionRevisionPayload(*functions.NewResourceLimits(100, "f2"), "ghcr.io/stackitcloud/example-image:0.1.0") + functionRevision, err := functionsClient.DefaultAPI.CreateFunctionRevision(context.Background(), projectId, *function.Id).CreateFunctionRevisionPayload(revisionData).Execute() + if err != nil || functionRevision.Id == nil { + fmt.Fprintf(os.Stderr, "Error when calling `CreateFunctionRevision`: %v\n", err) + os.Exit(1) + } + _, err = fmt.Printf("Function revision created at: %s\n", functionRevision.CreatedAt) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when printing out function revision: %v\n", err) + os.Exit(1) + } + + // Wait for creation of function revision + revision, err := wait.CreateRevisionWaitHandler(context.Background(), functionsClient.DefaultAPI, projectId, *function.Id, *functionRevision.Id).WaitWithContext(context.Background()) + if err != nil || revision.Id == nil { + fmt.Fprintf(os.Stderr, "Error when waiting for revision creation: %v\n", err) + os.Exit(1) + } + _, err = fmt.Printf("Function revision %s has been successfully deployed.\n", *revision.Id) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when printing out function revision: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/functions/go.mod b/examples/functions/go.mod new file mode 100644 index 0000000000..2a5401d5f4 --- /dev/null +++ b/examples/functions/go.mod @@ -0,0 +1,14 @@ +module github.com/stackitcloud/stackit-sdk-go/examples/functions + +go 1.25 + +// This is not needed in production. This is only here to point the golangci linter to the local version instead of the last release on GitHub. +replace github.com/stackitcloud/stackit-sdk-go/services/functions => ../../services/functions + +require github.com/stackitcloud/stackit-sdk-go/services/functions v0.1.0 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/stackitcloud/stackit-sdk-go/core v0.27.0 // indirect +) diff --git a/examples/functions/go.sum b/examples/functions/go.sum new file mode 100644 index 0000000000..042f4a71a5 --- /dev/null +++ b/examples/functions/go.sum @@ -0,0 +1,8 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/stackitcloud/stackit-sdk-go/core v0.27.0 h1:7lc6qStcFDFf8zHP4ORPa9joybw+Sa2LtB6BVjKQ+ek= +github.com/stackitcloud/stackit-sdk-go/core v0.27.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= diff --git a/go.work b/go.work index f790a0ea8e..5cd8d932ac 100644 --- a/go.work +++ b/go.work @@ -13,6 +13,7 @@ use ( ./examples/edge ./examples/errorhandling ./examples/experimental + ./examples/functions ./examples/iaas ./examples/intake ./examples/kms diff --git a/services/functions/CHANGELOG.md b/services/functions/CHANGELOG.md new file mode 100644 index 0000000000..426c759900 --- /dev/null +++ b/services/functions/CHANGELOG.md @@ -0,0 +1,3 @@ +## v0.1.0 +- Package `v1alphaapi`: + - **Feature**: initial release diff --git a/services/functions/NOTICE.txt b/services/functions/NOTICE.txt new file mode 100644 index 0000000000..fab5599d9f --- /dev/null +++ b/services/functions/NOTICE.txt @@ -0,0 +1,2 @@ +STACKIT Functions SDK for Go +Copyright 2024 - 2026 Schwarz IT KG diff --git a/services/functions/VERSION b/services/functions/VERSION new file mode 100644 index 0000000000..9ff151c5b3 --- /dev/null +++ b/services/functions/VERSION @@ -0,0 +1 @@ +v0.1.0 \ No newline at end of file diff --git a/services/functions/go.mod b/services/functions/go.mod index 0a764e4eab..0e965e624b 100644 --- a/services/functions/go.mod +++ b/services/functions/go.mod @@ -6,5 +6,6 @@ require github.com/stackitcloud/stackit-sdk-go/core v0.27.0 require ( github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 // indirect ) diff --git a/services/functions/v1alphaapi/wait/wait.go b/services/functions/v1alphaapi/wait/wait.go new file mode 100644 index 0000000000..60e43b9971 --- /dev/null +++ b/services/functions/v1alphaapi/wait/wait.go @@ -0,0 +1,37 @@ +package wait + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/stackitcloud/stackit-sdk-go/core/wait" + functions "github.com/stackitcloud/stackit-sdk-go/services/functions/v1alphaapi" +) + +// CreateRevisionWaitHandler will wait for Revision creation +func CreateRevisionWaitHandler(ctx context.Context, client functions.DefaultAPI, projectId, functionId, revisionId string) *wait.AsyncActionHandler[functions.Revision] { + waitConfig := wait.WaiterHelper[functions.Revision, functions.RevisionState]{ + FetchInstance: client.GetRevision(ctx, projectId, functionId, revisionId).Execute, + GetState: func(revision *functions.Revision) (functions.RevisionState, error) { + if revision.Id == nil || revision.State == nil { + return functions.REVISIONSTATE_UNKNOWN_DEFAULT_OPEN_API, fmt.Errorf("could not get revision id or state from response") + } + if *revision.Id == revisionId && *revision.State == functions.REVISIONSTATE_FAILED { + if revision.StateError != nil { + return *revision.State, fmt.Errorf("revision deployment failed: %s", *revision.StateError) + } + + return *revision.State, fmt.Errorf("revision deployment failed") + } + return *revision.State, nil + }, + ActiveState: []functions.RevisionState{functions.REVISIONSTATE_ACTIVE}, + ErrorState: []functions.RevisionState{functions.REVISIONSTATE_FAILED, functions.REVISIONSTATE_CANCELLED}, //nolint:misspell // british wording + DeleteHttpErrorStatusCodes: []int{http.StatusNotFound}, + } + handler := wait.New(waitConfig.Wait()) + handler.SetTimeout(10 * time.Minute) + return handler +} diff --git a/services/functions/v1alphaapi/wait/wait_test.go b/services/functions/v1alphaapi/wait/wait_test.go new file mode 100644 index 0000000000..d3c925f7c7 --- /dev/null +++ b/services/functions/v1alphaapi/wait/wait_test.go @@ -0,0 +1,141 @@ +package wait + +import ( + "context" + "testing" + "testing/synctest" + "time" + + "github.com/google/go-cmp/cmp" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + + functions "github.com/stackitcloud/stackit-sdk-go/services/functions/v1alphaapi" +) + +type mockSettings struct { + createFails bool + resourceStates []functions.RevisionState +} + +func newAPIMock(settings mockSettings) functions.DefaultAPI { + return &functions.DefaultAPIServiceMock{ + GetRevisionExecuteMock: utils.Ptr( + func(_ functions.ApiGetRevisionRequest) (*functions.Revision, error) { + if settings.createFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + state := settings.resourceStates[len(settings.resourceStates)-1] + return &functions.Revision{ + Id: utils.Ptr("rid"), + FunctionId: utils.Ptr("fid"), + State: utils.Ptr(state), + }, nil + }, + ), + } +} + +func TestCreateRevisionWaitHandler(t *testing.T) { + tests := []struct { + desc string + resourceStates []functions.RevisionState + wantErr bool + wantResp bool + wantState functions.RevisionState + }{ + { + desc: "create_succeeded", + resourceStates: []functions.RevisionState{ + functions.REVISIONSTATE_ACTIVE, + }, + wantErr: false, + wantResp: true, + wantState: functions.REVISIONSTATE_ACTIVE, + }, + { + desc: "revision_failed", + resourceStates: []functions.RevisionState{ + functions.REVISIONSTATE_FAILED, + }, + wantErr: true, + wantResp: false, + wantState: functions.REVISIONSTATE_FAILED, + }, + { + desc: "creating_then_succeeded", + resourceStates: []functions.RevisionState{ + functions.REVISIONSTATE_CREATING, + functions.REVISIONSTATE_ACTIVE, + }, + wantErr: false, + wantResp: true, + wantState: functions.REVISIONSTATE_ACTIVE, + }, + { + desc: "unknown", + resourceStates: []functions.RevisionState{ + functions.REVISIONSTATE_UNKNOWN_DEFAULT_OPEN_API, + }, + wantErr: true, + wantResp: false, + }, + { + desc: "create_api_error", + wantErr: true, + wantResp: false, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + apiClient := newAPIMock(mockSettings{ + createFails: tt.wantErr, + resourceStates: tt.resourceStates, + }) + + var wantRes *functions.Revision + if tt.wantResp { + wantRes = &functions.Revision{ + Id: utils.Ptr("rid"), + FunctionId: utils.Ptr("fid"), + State: utils.Ptr(tt.wantState), + } + } + + handler := CreateRevisionWaitHandler( + context.Background(), + apiClient, + "pid", + "fid", + "rid", + ) + + gotRes, err := handler. + SetTimeout(10 * time.Millisecond). + WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf( + "handler error = %v, wantErr %v", + err, + tt.wantErr, + ) + } + + if !cmp.Equal(gotRes, wantRes) { + t.Fatalf( + "handler gotRes = %v, want %v", + gotRes, + wantRes, + ) + } + }) + }) + } +}