package services import ( "context" "database/sql" "fmt" "strconv" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/require" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" sqlquery "edge-infra.dev/pkg/edge/api/sql" "edge-infra.dev/pkg/edge/constants" linkerd "edge-infra.dev/pkg/edge/linkerd" "edge-infra.dev/pkg/lib/uuid" "edge-infra.dev/pkg/sds/ien/topology" ) const ( // test cluster edge id clusterConfigClusterEdgeID = "1c52b9fa-1396-4ebf-a67a-23d06021774c" ) var ( namespaceLogLevelsPayloadInsert = "[]" clusterConfigRows = sqlmock.NewRows([]string{"cluster_config_edge_id", "cluster_edge_id", "config_key", "config_value"}) ) var ( // valid values validNamespace = "fluent-operator" validLevel = "ALERT" validMaximumLanOutage = int(linkerd.DefaultThickPosIdentityIssuerCertificateDurationHours) // invalid values invalidLogLevel = "NONSENSE" invalidMaximumLanOutage = 23 ) // Deprecated: Marked as DEPRECATED. Do not use var ( invalidL5dCertRenewal = -1 invalidL5dCertDuration = 0 validL5dCertDuration = int(linkerd.DefaultThinPosIdentityIssuerCertificateDurationHours) validL5dCertRenewal = int(linkerd.DefaultThinPosIdentityIssuerCertificateRenewBeforeHours) ) type clusterConfigTests struct { description string currentConfig *model.ClusterConfig inputConfig *model.UpdateClusterConfig expectedConfig *model.ClusterConfig expectedErr error } var ( trueValue = true falseValue = false rateLimitValid = "4mbit" rateLimitInvalid = "4" ) var clusterCfgTestCases = []clusterConfigTests{ { description: "create valid default configuration", currentConfig: nil, inputConfig: &model.UpdateClusterConfig{}, expectedConfig: defaultClusterConfig(clusterConfigClusterEdgeID), expectedErr: nil, }, { description: "change all default cluster configuration values", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ AcRelay: &falseValue, PxeEnabled: &falseValue, BootstrapAck: &trueValue, GatewayRateLimitingEnabled: &trueValue, EgressGatewayEnabled: &trueValue, UplinkRateLimit: &rateLimitValid, DownlinkRateLimit: &rateLimitValid, ThickPos: &trueValue, VpnEnabled: &trueValue, ClusterLogLevel: &validLevel, NamespaceLogLevels: []*model.NamespaceLogLevelPayload{ { Namespace: &validNamespace, Level: &validLevel, }, }, MaximumLanOutageHours: &validMaximumLanOutage, // TODO: Marked as DEPRECATED. Remove LinkerdIdentityIssuerCertDuration: &validL5dCertDuration, LinkerdIdentityIssuerCertRenewBefore: &validL5dCertRenewal, VncReadWriteAuthRequired: &trueValue, VncReadAuthRequired: &falseValue, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ AcRelay: &falseValue, PxeEnabled: &falseValue, BootstrapAck: &trueValue, GatewayRateLimitingEnabled: &trueValue, EgressGatewayEnabled: &trueValue, UplinkRateLimit: &rateLimitValid, DownlinkRateLimit: &rateLimitValid, ThickPos: &trueValue, VpnEnabled: &trueValue, ClusterLogLevel: &validLevel, NamespaceLogLevels: []*model.NamespaceLogLevelPayload{ { Namespace: &validNamespace, Level: &validLevel, }, }, MaximumLanOutageHours: &validMaximumLanOutage, // TODO: Marked as DEPRECATED. Remove LinkerdIdentityIssuerCertDuration: &validL5dCertDuration, LinkerdIdentityIssuerCertRenewBefore: &validL5dCertRenewal, VncReadWriteAuthRequired: &trueValue, VncReadAuthRequired: &falseValue, }), expectedErr: nil, }, { description: "update boot options with valid configuration", currentConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ AcRelay: &trueValue, PxeEnabled: &trueValue, BootstrapAck: &falseValue, }), inputConfig: &model.UpdateClusterConfig{ AcRelay: &falseValue, PxeEnabled: &falseValue, BootstrapAck: &trueValue, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ AcRelay: &falseValue, PxeEnabled: &falseValue, BootstrapAck: &trueValue, }), expectedErr: nil, }, { description: "update egress gateway options with valid configuration", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ GatewayRateLimitingEnabled: &trueValue, EgressGatewayEnabled: &trueValue, UplinkRateLimit: &rateLimitValid, DownlinkRateLimit: &rateLimitValid, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ GatewayRateLimitingEnabled: &trueValue, EgressGatewayEnabled: &trueValue, UplinkRateLimit: &rateLimitValid, DownlinkRateLimit: &rateLimitValid, }), expectedErr: nil, }, { description: "invalid egress gateway bandwidth limits", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ GatewayRateLimitingEnabled: &trueValue, EgressGatewayEnabled: &trueValue, UplinkRateLimit: &rateLimitInvalid, DownlinkRateLimit: &rateLimitInvalid, }, expectedConfig: nil, expectedErr: ErrInvalidRateLimit, }, { description: "thick pos topology setting", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ ThickPos: &trueValue, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ ThickPos: &trueValue, }), expectedErr: nil, }, { description: "vpn enablement cluster setting", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ VpnEnabled: &trueValue, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ VpnEnabled: &trueValue, }), expectedErr: nil, }, { description: "valid cluster log levels", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ ClusterLogLevel: &validLevel, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ ClusterLogLevel: &validLevel, }), expectedErr: nil, }, { description: "invalid cluster log levels", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ ClusterLogLevel: &invalidLogLevel, }, expectedConfig: nil, expectedErr: &ErrInvalidLogLevel{ LogLevel: invalidLogLevel, }, }, { description: "valid namespace log levels", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ NamespaceLogLevels: []*model.NamespaceLogLevelPayload{ { Namespace: &validNamespace, Level: &validLevel, }, }, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ NamespaceLogLevels: []*model.NamespaceLogLevelPayload{ { Namespace: &validNamespace, Level: &validLevel, }, }, }), expectedErr: nil, }, { description: "invalid namespace log levels", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ NamespaceLogLevels: []*model.NamespaceLogLevelPayload{ { Namespace: &validNamespace, Level: &invalidLogLevel, }, }, }, expectedConfig: nil, expectedErr: &ErrInvalidLogLevel{ LogLevel: invalidLogLevel, }, }, { description: "valid maximum lan outage duration in hours", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ MaximumLanOutageHours: &validMaximumLanOutage, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ MaximumLanOutageHours: &validMaximumLanOutage, }), expectedErr: nil, }, { description: "invalid maximum lan outage duration in hours", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ MaximumLanOutageHours: &invalidMaximumLanOutage, }, expectedConfig: nil, expectedErr: ErrInvalidMaximumLanOutageHours, }, // TODO: DEPRECATED test cases for linkerd identity cert. Remove { description: "valid linkerd cert duration and renew", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ LinkerdIdentityIssuerCertDuration: &validL5dCertDuration, LinkerdIdentityIssuerCertRenewBefore: &validL5dCertRenewal, }, expectedConfig: generateExpectedClusterConfig(&model.UpdateClusterConfig{ LinkerdIdentityIssuerCertDuration: &validL5dCertDuration, LinkerdIdentityIssuerCertRenewBefore: &validL5dCertRenewal, }), expectedErr: nil, }, { description: "invalid linkerd cert duration and renewal", currentConfig: defaultClusterConfig(clusterConfigClusterEdgeID), inputConfig: &model.UpdateClusterConfig{ LinkerdIdentityIssuerCertDuration: &invalidL5dCertDuration, LinkerdIdentityIssuerCertRenewBefore: &invalidL5dCertRenewal, }, expectedConfig: nil, expectedErr: topology.ErrInvalidCertificateDurationOrRenewBefore, //nolint:staticcheck // Allow existing usage of deprecated fields }, } func TestUpdateClusterConfig(t *testing.T) { for _, test := range clusterCfgTestCases { ctx := context.Background() passMsg := fmt.Sprintf("PASS: %s", test.description) failMsg := fmt.Sprintf("FAIL: %s", test.description) db, mock, err := generateMockDB(test.currentConfig, test.expectedConfig, test.inputConfig) require.NoError(t, err, failMsg) service := NewClusterConfigService(db) actualClusterCfg, err := service.UpdateClusterConfig(ctx, clusterConfigClusterEdgeID, test.inputConfig) if test.expectedErr != nil { require.Equal(t, test.expectedErr.Error(), err.Error(), failMsg) t.Log(passMsg) continue } require.NoError(t, err, failMsg) require.NotNil(t, test.expectedConfig, failMsg) require.NotNil(t, actualClusterCfg, failMsg) require.Equal(t, *test.expectedConfig, *actualClusterCfg, failMsg) require.NoError(t, mock.ExpectationsWereMet(), failMsg) t.Log(passMsg) } } // verfies that the default configuration is returned if // a new cluster configuration key is added to the api. // This ensures that older clusters that do not have the key registered // in the database yet, get the correct default values. func TestGetNewAddedKeyClusterConfig(t *testing.T) { ctx := context.Background() db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) defer db.Close() mock.ExpectQuery(sqlquery.GetClusterConfig). WithArgs(clusterConfigClusterEdgeID). WillReturnRows(clusterConfigRows) service := NewClusterConfigService(db) clusterCfg, err := service.GetClusterConfig(ctx, clusterConfigClusterEdgeID) require.NoError(t, err) require.Equal(t, defaultClusterConfig(clusterConfigClusterEdgeID), clusterCfg) } // This is key mapper function so it needs a test case func TestNLLPToNLL(t *testing.T) { testLogLevels := make([]*model.NamespaceLogLevelPayload, 3) validNamespace2 := "kube-system" validNamespace3 := "prometheus" validLevel2 := "EMERGENCY" testLogLevels[0] = &model.NamespaceLogLevelPayload{ Namespace: &validNamespace, Level: &validLevel, } testLogLevels[1] = &model.NamespaceLogLevelPayload{ Namespace: &validNamespace2, Level: &validLevel, } testLogLevels[2] = &model.NamespaceLogLevelPayload{ Namespace: &validNamespace3, Level: &validLevel2, } NLL := mapper.NLLPToNLL(testLogLevels) require.Len(t, NLL, 3) } // helper to generate a mock db and assertions for ClusterConfig func generateMockDB(currentCfg, expectedCfg *model.ClusterConfig, inputCfg *model.UpdateClusterConfig) (*sql.DB, sqlmock.Sqlmock, error) { db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) if err != nil { return nil, nil, err } mock.MatchExpectationsInOrder(false) if currentCfg != nil { mock.ExpectQuery(sqlquery.GetClusterConfig).WithArgs(clusterConfigClusterEdgeID). WillReturnRows(clusterConfigRows. AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, AcRelayKey, strconv.FormatBool(currentCfg.AcRelay)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, PxeEnabledKey, strconv.FormatBool(currentCfg.PxeEnabled)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, BootstrapAckKey, strconv.FormatBool(currentCfg.BootstrapAck)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, VpnEnabledKey, strconv.FormatBool(currentCfg.VpnEnabled)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, ThickPosKey, strconv.FormatBool(currentCfg.ThickPos)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, GatewayRateLimitingEnabledKey, strconv.FormatBool(currentCfg.GatewayRateLimitingEnabled)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, UplinkRateLimitKey, currentCfg.UplinkRateLimit). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, DownlinkRateLimitKey, currentCfg.DownlinkRateLimit). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, ClusterLogLevelKey, currentCfg.ClusterLogLevel). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, NamespaceLogLevelsKey, namespaceLogLevelsPayloadInsert). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, MaximumLanOutageHoursKey, strconv.FormatInt(int64(currentCfg.MaximumLanOutageHours), 10)). //TODO: Marked as DEPRECATED. Remove //nolint:staticcheck // Allow existing usage of deprecated fields AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, constants.LinkerdIdentityIssuerCertDuration, strconv.FormatInt(int64(currentCfg.LinkerdIdentityIssuerCertDuration), 10)). //TODO: Marked as DEPRECATED. Remove //nolint:staticcheck // Allow existing usage of deprecated fields AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, constants.LinkerdIdentityIssuerCertRenewBefore, strconv.FormatInt(int64(currentCfg.LinkerdIdentityIssuerCertRenewBefore), 10)). AddRow(uuid.New().UUID, clusterConfigClusterEdgeID, EgressGatewayEnabledKey, strconv.FormatBool(currentCfg.EgressGatewayEnabled))) } else { mock.ExpectQuery(sqlquery.GetClusterConfig).WithArgs(clusterConfigClusterEdgeID).WillReturnRows(clusterConfigRows) } if expectedCfg == nil { mock.ExpectBegin() mock.ExpectCommit() return db, mock, nil } mock.ExpectBegin() if inputCfg.AcRelay != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, AcRelayKey, strconv.FormatBool(expectedCfg.AcRelay), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.PxeEnabled != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, PxeEnabledKey, strconv.FormatBool(expectedCfg.PxeEnabled), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.BootstrapAck != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, BootstrapAckKey, strconv.FormatBool(expectedCfg.BootstrapAck), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.VpnEnabled != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, VpnEnabledKey, strconv.FormatBool(expectedCfg.VpnEnabled), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.ThickPos != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, ThickPosKey, strconv.FormatBool(expectedCfg.ThickPos), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.EgressGatewayEnabled != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, EgressGatewayEnabledKey, strconv.FormatBool(expectedCfg.EgressGatewayEnabled), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.GatewayRateLimitingEnabled != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, GatewayRateLimitingEnabledKey, strconv.FormatBool(expectedCfg.GatewayRateLimitingEnabled), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.UplinkRateLimit != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, UplinkRateLimitKey, expectedCfg.UplinkRateLimit, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.DownlinkRateLimit != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, DownlinkRateLimitKey, expectedCfg.DownlinkRateLimit, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.ClusterLogLevel != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, ClusterLogLevelKey, expectedCfg.ClusterLogLevel, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.NamespaceLogLevels != nil { namespaces, _ := mapper.NLLPToJSON(inputCfg.NamespaceLogLevels) mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, NamespaceLogLevelsKey, namespaces, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.MaximumLanOutageHours != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, MaximumLanOutageHoursKey, strconv.FormatInt(int64(expectedCfg.MaximumLanOutageHours), 10), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } //nolint:staticcheck // Allow existing usage of deprecated fields if inputCfg.LinkerdIdentityIssuerCertDuration != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, constants.LinkerdIdentityIssuerCertDuration, strconv.FormatInt(int64(expectedCfg.LinkerdIdentityIssuerCertDuration), 10), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } //nolint:staticcheck // Allow existing usage of deprecated fields if inputCfg.LinkerdIdentityIssuerCertRenewBefore != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, constants.LinkerdIdentityIssuerCertRenewBefore, strconv.FormatInt(int64(expectedCfg.LinkerdIdentityIssuerCertRenewBefore), 10), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.VncReadWriteAuthRequired != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, VncReadWriteAuthRequired, strconv.FormatBool(*expectedCfg.VncReadWriteAuthRequired), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } if inputCfg.VncReadAuthRequired != nil { mock.ExpectExec(sqlquery.UpdateClusterConfig). WithArgs(clusterConfigClusterEdgeID, VncReadAuthRequired, strconv.FormatBool(*expectedCfg.VncReadAuthRequired), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) } mock.ExpectCommit() return db, mock, nil } // overrides the default config with expected config changes func generateExpectedClusterConfig(expectedClusterCfg *model.UpdateClusterConfig) *model.ClusterConfig { clusterCfg := defaultClusterConfig(clusterConfigClusterEdgeID) mapUpdateClusterConfig(clusterCfg, expectedClusterCfg) return clusterCfg }