diff --git a/internal/cmd/server/describe/describe.go b/internal/cmd/server/describe/describe.go index 85444e6ec..ee6e32ea6 100644 --- a/internal/cmd/server/describe/describe.go +++ b/internal/cmd/server/describe/describe.go @@ -118,7 +118,12 @@ func outputResult(p *print.Printer, outputFormat string, server *iaas.Server) er return nil case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(server, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + // 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()) 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 29eb51222..8c1596cbb 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -158,7 +158,12 @@ func outputResult(p *print.Printer, outputFormat string, servers []iaas.Server) return nil case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(servers, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + // 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()) 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 2db0936b8..4c362fea2 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -13,6 +13,7 @@ import ( "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-sdk-go/services/iaas" ) // Ptr Returns the pointer to any type T @@ -153,3 +154,99 @@ 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.CreateServerPayloadBootVolume `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.CreateServerPayloadNetworking `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"` +} + +// 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, + } +} + +// 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 +} diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 86588bad8..79fdbae5e 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -3,8 +3,10 @@ package utils import ( "reflect" "testing" + "time" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/viper" "github.com/stackitcloud/stackit-cli/internal/pkg/config" @@ -248,3 +250,297 @@ func TestConvertStringMapToInterfaceMap(t *testing.T) { }) } } + +func TestConvertToBase64PatchedServer(t *testing.T) { + now := time.Now() + userData := []byte("test") + emptyUserData := []byte("") + + 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: Ptr("test-server"), + Status: Ptr("ACTIVE"), + AvailabilityZone: Ptr("eu01-1"), + MachineType: Ptr("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: Ptr("test-server-2"), + Status: Ptr("STOPPED"), + AvailabilityZone: Ptr("eu01-2"), + MachineType: Ptr("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: Ptr("test-server-3"), + Status: Ptr("CREATING"), + AvailabilityZone: Ptr("eu01-3"), + MachineType: Ptr("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, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertToBase64PatchedServer(tt.input) + + if result == nil && tt.expected == nil { + return + } + + if (result == nil && tt.expected != nil) || (result != nil && tt.expected == nil) { + t.Errorf("ConvertToBase64PatchedServer() = %v, want %v", result, tt.expected) + return + } + + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("ConvertToBase64PatchedServer() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestConvertToBase64PatchedServers(t *testing.T) { + now := time.Now() + userData1 := []byte("test1") + userData2 := []byte("test2") + emptyUserData := []byte("") + + 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: Ptr("test-server-1"), + Status: Ptr("ACTIVE"), + MachineType: Ptr("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: Ptr("test-server-1"), + Status: Ptr("ACTIVE"), + MachineType: Ptr("t1.1"), + AvailabilityZone: Ptr("eu01-1"), + UserData: &userData1, + CreatedAt: &now, + }, + { + Id: Ptr("server-2"), + Name: Ptr("test-server-2"), + Status: Ptr("STOPPED"), + MachineType: Ptr("t1.2"), + AvailabilityZone: Ptr("eu01-2"), + UserData: &userData2, + }, + { + Id: Ptr("server-3"), + Name: Ptr("test-server-3"), + Status: Ptr("CREATING"), + MachineType: Ptr("t1.3"), + AvailabilityZone: Ptr("eu01-3"), + UserData: &emptyUserData, + }, + { + Id: Ptr("server-4"), + Name: Ptr("test-server-4"), + Status: Ptr("ERROR"), + MachineType: Ptr("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) + } + }) + } +}