From 38620907fba1507da8d6ef9693efa6bf0a16551d Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 16 Jun 2026 15:41:19 +0200 Subject: [PATCH 1/2] fix(iaas): encoding and decoding of user data in server commands relates to STACKITCLI-412 --- internal/cmd/server/create/create.go | 8 +- internal/cmd/server/create/create_test.go | 13 +- internal/cmd/server/describe/describe.go | 7 +- internal/cmd/server/list/list.go | 7 +- internal/pkg/utils/utils.go | 102 +------ internal/pkg/utils/utils_test.go | 308 ---------------------- 6 files changed, 22 insertions(+), 423 deletions(-) diff --git a/internal/cmd/server/create/create.go b/internal/cmd/server/create/create.go index a9d5f5152..d476c1883 100644 --- a/internal/cmd/server/create/create.go +++ b/internal/cmd/server/create/create.go @@ -2,6 +2,7 @@ package create import ( "context" + "encoding/base64" "fmt" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -275,6 +276,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateServerRequest { req := apiClient.DefaultAPI.CreateServer(ctx, model.ProjectId, model.Region) + var userData *string + if model.UserData != nil { + userData = utils.Ptr(base64.StdEncoding.EncodeToString([]byte(*model.UserData))) + } + payload := iaas.CreateServerPayload{ Name: model.Name, MachineType: model.MachineType, @@ -285,7 +291,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli KeypairName: model.KeypairName, SecurityGroups: model.SecurityGroups, ServiceAccountMails: model.ServiceAccountMails, - UserData: model.UserData, + UserData: userData, Volumes: model.Volumes, Labels: model.Labels, } diff --git a/internal/cmd/server/create/create_test.go b/internal/cmd/server/create/create_test.go index a554eb508..3eecfa4e5 100644 --- a/internal/cmd/server/create/create_test.go +++ b/internal/cmd/server/create/create_test.go @@ -125,7 +125,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateServerPayload)) iaas.Create KeypairName: utils.Ptr("test-keypair-name"), SecurityGroups: []string{"test-security-groups"}, ServiceAccountMails: []string{"test-service-account"}, - UserData: utils.Ptr("test-user-data"), + UserData: utils.Ptr("dGVzdC11c2VyLWRhdGE="), Volumes: []string{testVolumeId}, BootVolume: &iaas.BootVolume{ PerformanceClass: utils.Ptr("test-perf-class"), @@ -397,6 +397,17 @@ func TestBuildRequest(t *testing.T) { *request = request.CreateServerPayload(payload) }), }, + { + description: "with user data", + model: fixtureInputModel(func(model *inputModel) { + model.UserData = utils.Ptr("cloud-init data") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateServerRequest) { + payload := fixturePayload() + payload.UserData = utils.Ptr("Y2xvdWQtaW5pdCBkYXRh") + *request = request.CreateServerPayload(payload) + }), + }, } for _, tt := range tests { diff --git a/internal/cmd/server/describe/describe.go b/internal/cmd/server/describe/describe.go index 37f20e04b..0bfe115fb 100644 --- a/internal/cmd/server/describe/describe.go +++ b/internal/cmd/server/describe/describe.go @@ -111,12 +111,7 @@ func outputResult(p *print.Printer, outputFormat string, server *iaas.Server) er return nil case print.YAMLOutputFormat: - // This is a temporary workaround to get the desired base64 encoded yaml output for userdata - // and will be replaced by a fix in the Go-SDK - // ref: https://jira.schwarz/browse/STACKITSDK-246 - patchedServer := utils.ConvertToBase64PatchedServer(server) - - details, err := yaml.MarshalWithOptions(patchedServer, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + details, err := yaml.MarshalWithOptions(server, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) if err != nil { return fmt.Errorf("marshal server: %w", err) } diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 6bf36485c..a7791b99a 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -148,12 +148,7 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, servers [ return nil case print.YAMLOutputFormat: - // This is a temporary workaround to get the desired base64 encoded yaml output for userdata - // and will be replaced by a fix in the Go-SDK - // ref: https://jira.schwarz/browse/STACKITSDK-246 - patchedServers := utils.ConvertToBase64PatchedServers(servers) - - details, err := yaml.MarshalWithOptions(patchedServers, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + details, err := yaml.MarshalWithOptions(servers, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) if err != nil { return fmt.Errorf("marshal server: %w", err) } diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index 69d3b32c3..a5a2f6257 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -12,10 +12,8 @@ import ( "github.com/inhies/go-bytesize" "github.com/spf13/cobra" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" ) // Ptr Returns the pointer to any type T @@ -165,104 +163,6 @@ func ConvertStringMapToInterfaceMap(m *map[string]string) *map[string]interface{ return &result } -// Base64Bytes implements yaml.Marshaler to convert []byte to base64 strings -// ref: https://carlosbecker.com/posts/go-custom-marshaling -type Base64Bytes []byte - -// MarshalYAML implements yaml.Marshaler -func (b Base64Bytes) MarshalYAML() (interface{}, error) { - if len(b) == 0 { - return "", nil - } - return base64.StdEncoding.EncodeToString(b), nil -} - -type Base64PatchedServer struct { - Id *string `json:"id,omitempty"` - Name *string `json:"name,omitempty"` - Status *string `json:"status,omitempty"` - AvailabilityZone *string `json:"availabilityZone,omitempty"` - BootVolume *iaas.BootVolume `json:"bootVolume,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty"` - ErrorMessage *string `json:"errorMessage,omitempty"` - PowerStatus *string `json:"powerStatus,omitempty"` - AffinityGroup *string `json:"affinityGroup,omitempty"` - ImageId *string `json:"imageId,omitempty"` - KeypairName *string `json:"keypairName,omitempty"` - MachineType *string `json:"machineType,omitempty"` - Labels map[string]interface{} `json:"labels,omitempty"` - LaunchedAt *time.Time `json:"launchedAt,omitempty"` - MaintenanceWindow *iaas.ServerMaintenance `json:"maintenanceWindow,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - Networking *iaas.ServerNetworking `json:"networking,omitempty"` - Nics []iaas.ServerNetwork `json:"nics,omitempty"` - SecurityGroups []string `json:"securityGroups,omitempty"` - ServiceAccountMails []string `json:"serviceAccountMails,omitempty"` - UpdatedAt *time.Time `json:"updatedAt,omitempty"` - UserData *Base64Bytes `json:"userData,omitempty"` - Volumes []string `json:"volumes,omitempty"` - Agent *iaas.ServerAgent `json:"agent,omitempty"` -} - -// ConvertToBase64PatchedServer converts an iaas.Server to Base64PatchedServer -// This is a temporary workaround to get the desired base64 encoded yaml output for userdata -// and will be replaced by a fix in the Go-SDK -// ref: https://jira.schwarz/browse/STACKITSDK-246 -func ConvertToBase64PatchedServer(server *iaas.Server) *Base64PatchedServer { - if server == nil { - return nil - } - - var userData *Base64Bytes - if server.UserData != nil { - userData = Ptr(Base64Bytes(*server.UserData)) - } - - return &Base64PatchedServer{ - Id: server.Id, - Name: &server.Name, - Status: server.Status, - AvailabilityZone: server.AvailabilityZone, - BootVolume: server.BootVolume, - CreatedAt: server.CreatedAt, - ErrorMessage: server.ErrorMessage, - PowerStatus: server.PowerStatus, - AffinityGroup: server.AffinityGroup, - ImageId: server.ImageId, - KeypairName: server.KeypairName, - MachineType: &server.MachineType, - Labels: server.Labels, - LaunchedAt: server.LaunchedAt, - MaintenanceWindow: server.MaintenanceWindow, - Metadata: server.Metadata, - Networking: server.Networking, - Nics: server.Nics, - SecurityGroups: server.SecurityGroups, - ServiceAccountMails: server.ServiceAccountMails, - UpdatedAt: server.UpdatedAt, - UserData: userData, - Volumes: server.Volumes, - Agent: server.Agent, - } -} - -// ConvertToBase64PatchedServers converts a slice of iaas.Server to a slice of Base64PatchedServer -// This is a temporary workaround to get the desired base64 encoded yaml output for userdata -// and will be replaced by a fix in the Go-SDK -// ref: https://jira.schwarz/browse/STACKITSDK-246 -func ConvertToBase64PatchedServers(servers []iaas.Server) []Base64PatchedServer { - if servers == nil { - return nil - } - - result := make([]Base64PatchedServer, len(servers)) - for i := range servers { - result[i] = *ConvertToBase64PatchedServer(&servers[i]) - } - - return result -} - // GetSliceFromPointer returns the value of a pointer to a slice of type T. // If the pointer is nil, it returns an empty slice. func GetSliceFromPointer[T any](s *[]T) []T { diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index a36e336ec..df6a6c414 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -3,11 +3,8 @@ package utils import ( "reflect" "testing" - "time" - "github.com/google/go-cmp/cmp" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" "github.com/spf13/viper" @@ -268,311 +265,6 @@ func TestConvertStringMapToInterfaceMap(t *testing.T) { } } -func TestConvertToBase64PatchedServer(t *testing.T) { - now := time.Now() - userData := "test" - emptyUserData := "" - - tests := []struct { - name string - input *iaas.Server - expected *Base64PatchedServer - }{ - { - name: "nil input", - input: nil, - expected: nil, - }, - { - name: "server with user data", - input: &iaas.Server{ - Id: Ptr("server-123"), - Name: "test-server", - Status: Ptr("ACTIVE"), - AvailabilityZone: Ptr("eu01-1"), - MachineType: "t1.1", - UserData: &userData, - CreatedAt: &now, - PowerStatus: Ptr("RUNNING"), - AffinityGroup: Ptr("group-1"), - ImageId: Ptr("image-123"), - KeypairName: Ptr("keypair-1"), - }, - expected: &Base64PatchedServer{ - Id: Ptr("server-123"), - Name: Ptr("test-server"), - Status: Ptr("ACTIVE"), - AvailabilityZone: Ptr("eu01-1"), - MachineType: Ptr("t1.1"), - UserData: Ptr(Base64Bytes(userData)), - CreatedAt: &now, - PowerStatus: Ptr("RUNNING"), - AffinityGroup: Ptr("group-1"), - ImageId: Ptr("image-123"), - KeypairName: Ptr("keypair-1"), - }, - }, - { - name: "server with empty user data", - input: &iaas.Server{ - Id: Ptr("server-456"), - Name: "test-server-2", - Status: Ptr("STOPPED"), - AvailabilityZone: Ptr("eu01-2"), - MachineType: "t1.2", - UserData: &emptyUserData, - }, - expected: &Base64PatchedServer{ - Id: Ptr("server-456"), - Name: Ptr("test-server-2"), - Status: Ptr("STOPPED"), - AvailabilityZone: Ptr("eu01-2"), - MachineType: Ptr("t1.2"), - UserData: Ptr(Base64Bytes(emptyUserData)), - }, - }, - { - name: "server without user data", - input: &iaas.Server{ - Id: Ptr("server-789"), - Name: "test-server-3", - Status: Ptr("CREATING"), - AvailabilityZone: Ptr("eu01-3"), - MachineType: "t1.3", - UserData: nil, - }, - expected: &Base64PatchedServer{ - Id: Ptr("server-789"), - Name: Ptr("test-server-3"), - Status: Ptr("CREATING"), - AvailabilityZone: Ptr("eu01-3"), - MachineType: Ptr("t1.3"), - UserData: nil, - }, - }, - { - name: "server with agent", - input: &iaas.Server{ - Id: Ptr("server-456"), - Name: "test-server-2", - Status: Ptr("STOPPED"), - AvailabilityZone: Ptr("eu01-2"), - MachineType: "t1.2", - UserData: &emptyUserData, - Agent: &iaas.ServerAgent{Provisioned: Ptr(true)}, - }, - expected: &Base64PatchedServer{ - Id: Ptr("server-456"), - Name: Ptr("test-server-2"), - Status: Ptr("STOPPED"), - AvailabilityZone: Ptr("eu01-2"), - MachineType: Ptr("t1.2"), - UserData: Ptr(Base64Bytes(emptyUserData)), - Agent: Ptr(iaas.ServerAgent{Provisioned: Ptr(true)}), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ConvertToBase64PatchedServer(tt.input) - - if diff := cmp.Diff(tt.expected, result); diff != "" { - t.Errorf("ConvertToBase64PatchedServer() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestConvertToBase64PatchedServers(t *testing.T) { - now := time.Now() - userData1 := "test1" - userData2 := "test2" - emptyUserData := "" - - tests := []struct { - name string - input []iaas.Server - expected []Base64PatchedServer - }{ - { - name: "nil input", - input: nil, - expected: nil, - }, - { - name: "empty slice", - input: []iaas.Server{}, - expected: []Base64PatchedServer{}, - }, - { - name: "single server with user data", - input: []iaas.Server{ - { - Id: Ptr("server-1"), - Name: "test-server-1", - Status: Ptr("ACTIVE"), - MachineType: "t1.1", - AvailabilityZone: Ptr("eu01-1"), - UserData: &userData1, - CreatedAt: &now, - }, - }, - expected: []Base64PatchedServer{ - { - Id: Ptr("server-1"), - Name: Ptr("test-server-1"), - Status: Ptr("ACTIVE"), - MachineType: Ptr("t1.1"), - AvailabilityZone: Ptr("eu01-1"), - UserData: Ptr(Base64Bytes(userData1)), - CreatedAt: &now, - }, - }, - }, - { - name: "multiple servers mixed", - input: []iaas.Server{ - { - Id: Ptr("server-1"), - Name: "test-server-1", - Status: Ptr("ACTIVE"), - MachineType: "t1.1", - AvailabilityZone: Ptr("eu01-1"), - UserData: &userData1, - CreatedAt: &now, - }, - { - Id: Ptr("server-2"), - Name: "test-server-2", - Status: Ptr("STOPPED"), - MachineType: "t1.2", - AvailabilityZone: Ptr("eu01-2"), - UserData: &userData2, - }, - { - Id: Ptr("server-3"), - Name: "test-server-3", - Status: Ptr("CREATING"), - MachineType: "t1.3", - AvailabilityZone: Ptr("eu01-3"), - UserData: &emptyUserData, - }, - { - Id: Ptr("server-4"), - Name: "test-server-4", - Status: Ptr("ERROR"), - MachineType: "t1.4", - AvailabilityZone: Ptr("eu01-4"), - UserData: nil, - }, - }, - expected: []Base64PatchedServer{ - { - Id: Ptr("server-1"), - Name: Ptr("test-server-1"), - Status: Ptr("ACTIVE"), - MachineType: Ptr("t1.1"), - AvailabilityZone: Ptr("eu01-1"), - UserData: Ptr(Base64Bytes(userData1)), - CreatedAt: &now, - }, - { - Id: Ptr("server-2"), - Name: Ptr("test-server-2"), - Status: Ptr("STOPPED"), - MachineType: Ptr("t1.2"), - AvailabilityZone: Ptr("eu01-2"), - UserData: Ptr(Base64Bytes(userData2)), - }, - { - Id: Ptr("server-3"), - Name: Ptr("test-server-3"), - Status: Ptr("CREATING"), - MachineType: Ptr("t1.3"), - AvailabilityZone: Ptr("eu01-3"), - UserData: Ptr(Base64Bytes(emptyUserData)), - }, - { - Id: Ptr("server-4"), - Name: Ptr("test-server-4"), - Status: Ptr("ERROR"), - MachineType: Ptr("t1.4"), - AvailabilityZone: Ptr("eu01-4"), - UserData: nil, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ConvertToBase64PatchedServers(tt.input) - - if result == nil && tt.expected == nil { - return - } - - if (result == nil && tt.expected != nil) || (result != nil && tt.expected == nil) { - t.Errorf("ConvertToBase64PatchedServers() = %v, want %v", result, tt.expected) - return - } - - if len(result) != len(tt.expected) { - t.Errorf("ConvertToBase64PatchedServers() length = %d, want %d", len(result), len(tt.expected)) - return - } - - for i, server := range result { - if !reflect.DeepEqual(server, tt.expected[i]) { - t.Errorf("ConvertToBase64PatchedServers() [%d] = %v, want %v", i, server, tt.expected[i]) - } - } - }) - } -} - -func TestBase64Bytes_MarshalYAML(t *testing.T) { - tests := []struct { - name string - input Base64Bytes - expected interface{} - }{ - { - name: "empty bytes", - input: Base64Bytes{}, - expected: "", - }, - { - name: "nil bytes", - input: Base64Bytes(nil), - expected: "", - }, - { - name: "simple text", - input: Base64Bytes("test"), - expected: "dGVzdA==", - }, - { - name: "special characters", - input: Base64Bytes("test@#$%"), - expected: "dGVzdEAjJCU=", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.input.MarshalYAML() - if err != nil { - t.Errorf("MarshalYAML() error = %v", err) - return - } - if result != tt.expected { - t.Errorf("MarshalYAML() = %v, want %v", result, tt.expected) - } - }) - } -} func TestGetSliceFromPointer(t *testing.T) { tests := []struct { name string From 4200802d94c17a8380bdf0318cacb28aa7ae3123 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 16 Jun 2026 16:45:08 +0200 Subject: [PATCH 2/2] fix linter issue --- internal/pkg/utils/utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index a5a2f6257..bb2b36561 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -12,8 +12,10 @@ import ( "github.com/inhies/go-bytesize" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) // Ptr Returns the pointer to any type T