diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 61133af91..b59a77d7e 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -7,6 +7,7 @@ - [Debug Console](flagr_debugging.md) - Server Configuration - [Env](flagr_env.md) + - [Migrations](flagr_migrations.md) - Client SDKs - [Ruby SDK 🔗](https://github.com/openflagr/rbflagr) - [Go SDK 🔗](https://github.com/openflagr/goflagr) diff --git a/docs/api_docs/bundle.yaml b/docs/api_docs/bundle.yaml index 821852602..a33a629c5 100644 --- a/docs/api_docs/bundle.yaml +++ b/docs/api_docs/bundle.yaml @@ -927,6 +927,21 @@ paths: description: generic error response schema: $ref: '#/definitions/error' + /migrations: + get: + tags: + - migration + operationId: runMigrations + description: Run migrations + responses: + '200': + description: OK + schema: + $ref: '#/definitions/migrationStatus' + default: + description: migration status with error code + schema: + $ref: '#/definitions/migrationStatus' /export/sqlite: get: tags: @@ -1459,6 +1474,18 @@ definitions: properties: status: type: string + migrationStatus: + type: object + properties: + code: + type: integer + migrations: + type: array + x-omitempty: true + items: + type: string + message: + type: string error: type: object required: diff --git a/docs/flagr_migrations.md b/docs/flagr_migrations.md new file mode 100644 index 000000000..08fc64592 --- /dev/null +++ b/docs/flagr_migrations.md @@ -0,0 +1,53 @@ +# Migrations + +Users can create flag collections as yaml files within a migration directory and run flagr to insert/modify flags. +This allows for developers to create flags as deployment assets and allows for migrating flags through various environments whilst keeping keys stable. + +Each found flag is upserted into the database. Then the flag has all segments, variants and tags removed then replaced with the new flag properties. + +As an example, create a file named `migrations/202403030000.yaml` with the following content: +```yaml +--- +# this is a basic flag +- key: SIMPLE-FLAG-1 + description: a toggle for just one user + enabled: true + segments: + - description: flag for just for one email test@test.com + rank: 0 + rolloutPercent: 100 + constraints: + - property: email + operator: EQ + value: '"test@test.com"' + distributions: + - variantKey: "on" + percent: 100 + - rank: 1 + rolloutPercent: 100 + constraints: [] + distributions: + - variantKey: "off" + percent: 100 + variants: + - key: "off" + attachment: {} + - key: "on" + attachment: {} + entityType: User + dataRecordsEnabled: true + +``` + +```shell +$ flagr -m +INFO[0146] 1 new migrations completed (1 total) +``` +Once the application is ran, flagr will scan the migration files, insert them into the db and shut down. + +### Config +Location of yaml configs can be set with either argument or env var. +``` +FLAGR_MIGRATION_PATH=./migrations/ ./flagr -m +./flagr -m --migrationPath=`pwd`/migrations +``` diff --git a/go.mod b/go.mod index 87e5eafb2..4e91cd9bb 100644 --- a/go.mod +++ b/go.mod @@ -147,4 +147,5 @@ require ( modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect modernc.org/sqlite v1.23.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 28ed0b7fa..0a181f9b5 100644 --- a/go.sum +++ b/go.sum @@ -714,3 +714,5 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/migrations/.schema.json b/migrations/.schema.json new file mode 100644 index 000000000..e4c5ffa1f --- /dev/null +++ b/migrations/.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "segments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "rank": { + "type": "integer" + }, + "rolloutPercent": { + "type": "integer" + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "property", + "operator", + "value" + ] + } + }, + "distributions": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "variantKey": { + "type": "string" + }, + "percent": { + "type": "integer" + } + }, + "required": [ + "variantKey", + "percent" + ] + } + ] + } + }, + "required": [ + "rank", + "rolloutPercent", + "distributions" + ] + } + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "attachment": { + "type": "object" + } + }, + "required": [ + "key" + ] + } + }, + "entityType": { + "type": "string" + }, + "dataRecordsEnabled": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": + { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ] + } + } + }, + "required": [ + "key", + "description", + "enabled" + ] + } +} diff --git a/pkg/entity/db.go b/pkg/entity/db.go index a29150b96..2b48ca246 100644 --- a/pkg/entity/db.go +++ b/pkg/entity/db.go @@ -32,6 +32,7 @@ var AutoMigrateTables = []interface{}{ Variant{}, Tag{}, FlagEntityType{}, + FlagMigration{}, } func connectDB() (db *gorm.DB, err error) { diff --git a/pkg/entity/flag_migration.go b/pkg/entity/flag_migration.go new file mode 100644 index 000000000..22c287100 --- /dev/null +++ b/pkg/entity/flag_migration.go @@ -0,0 +1,9 @@ +package entity + +import "gorm.io/gorm" + +type FlagMigration struct { + gorm.Model + + Name string `gorm:"type:varchar(64);uniqueIndex:idx_flag_migration_name"` +} diff --git a/pkg/handler/flag_migrations.go b/pkg/handler/flag_migrations.go new file mode 100644 index 000000000..0a2e1fb33 --- /dev/null +++ b/pkg/handler/flag_migrations.go @@ -0,0 +1,215 @@ +package handler + +import ( + "fmt" + "github.com/go-openapi/runtime/middleware" + "github.com/openflagr/flagr/pkg/entity" + "github.com/openflagr/flagr/pkg/mapper/entity_restapi/r2e" + "github.com/openflagr/flagr/pkg/util" + "github.com/openflagr/flagr/swagger_gen/models" + "github.com/openflagr/flagr/swagger_gen/restapi/operations/migration" + "github.com/sirupsen/logrus" + "github.com/spf13/cast" + "gorm.io/gorm/clause" + "os" + "sigs.k8s.io/yaml" + "strings" +) + +type FlagMigrationOptions struct { + Run bool `long:"migrations" short:"m" description:"Run Flag Migrations and Exit"` + Path string `long:"migrationPath" description:"Migration files path" env:"FLAGR_MIGRATION_PATH"` +} + +func FlagMigrations(path string) (string, error) { + return migrateFromDir(path) +} +var runMigrationsHandler = func(migration.RunMigrationsParams) middleware.Responder { + logrus.Info("Running migrations") + migrationPath, hasMigrationPath := os.LookupEnv("FLAGR_MIGRATION_PATH") + if !hasMigrationPath { + migrationPath = "migrations" + } + status, err := migrateFromDir(migrationPath) + if err != nil { + status = fmt.Sprintf("error running migrations: %v", err); + return migration.NewRunMigrationsDefault(500).WithPayload(&models.MigrationStatus{Message: status, Code: 500}) + } + + tx := getDB() + + var names []string + tx.Model(&entity.FlagMigration{}).Pluck("name", &names) + + return migration.NewRunMigrationsOK().WithPayload(&models.MigrationStatus{Message: status, Migrations: names, Code: 200}) +} + +func migrateFromDir(dir string) (string, error) { + tx := getDB() + fms := []entity.FlagMigration{} + + files, err := os.ReadDir(dir) + if err != nil { + logrus.WithField("err", err).Errorf("cannot read directory for migrations: %v %v", dir, err) + return "", err + } + + completedFiles := make(map[string]bool) + + tx.Find(&fms) + + for _, fm := range fms { + completedFiles[fm.Name] = true + } + + completed := 0 + for _, file := range files { + filename := file.Name() + if strings.HasSuffix(filename, ".yaml") && !completedFiles[filename] { + flags, err := readMigrationFile(dir, filename) + if err != nil { + continue + } + + completed = migrateFlags(flags, filename) + completed + } + + } + + logrus.Infof("%d new migrations completed", completed) + return fmt.Sprintf("%d new migrations completed", completed), nil +} + +func migrateFlags(flags []models.Flag, filename string) int { + for _, flagModel := range flags { + if f, err := migrateFlag(flagModel); err == nil { + entity.SaveFlagSnapshot(getDB(), util.SafeUint(f.ID), "migration "+filename) + } + } + + fm := entity.FlagMigration{Name: filename} + getDB().Create(&fm) + return 1 +} + +func migrateFlag(flagModel models.Flag) (*entity.Flag, error) { + flag := &entity.Flag{ + EntityType: flagModel.EntityType, + Key: flagModel.Key, + Description: util.SafeString(flagModel.Description), + DataRecordsEnabled: cast.ToBool(flagModel.DataRecordsEnabled), + Enabled: cast.ToBool(flagModel.Enabled), + Notes: util.SafeStringWithDefault(flagModel.Notes, ""), + } + + tx := getDB() + + //upsert + tx.Where(entity.Flag{Key: flagModel.Key}).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + UpdateAll: true, + }).Create(&flag) + + tx = entity.PreloadSegmentsVariantsTags(getDB()) + + tx.Where(entity.Flag{Key: flagModel.Key}).First(&flag) + + // delete the association + deleteTags(flag) + + // delete the entity (keeps associations + deleteVariants(flag) + deleteSegments(flag) + + addTags(flagModel, flag) + + // used to map variant to distribution + variantMap := saveVariants(flagModel, flag) + + saveSegments(flagModel, flag, variantMap) + + return flag, nil +} + +func saveSegments(flagModel models.Flag, flag *entity.Flag, variantMap map[string]int64) { + for _, segmentModel := range flagModel.Segments { + segment := entity.Segment{ + Description: util.SafeString(segmentModel.Description), + RolloutPercent: uint(*segmentModel.RolloutPercent), + Rank: uint(*segmentModel.Rank), + } + // save segment to flag + getDB().Model(flag).Association("Segments").Append(&segment) + // map distribution to variant + for _, distribution := range segmentModel.Distributions { + variantId := variantMap[util.SafeString(distribution.VariantKey)] + distribution.VariantID = &variantId + } + segment.Distributions = r2e.MapDistributions(segmentModel.Distributions, segment.ID) + segment.Constraints = r2e.MapConstraints(segmentModel.Constraints, segment.ID) + getDB().Save(&segment) + } +} + +func saveVariants(flagModel models.Flag, flag *entity.Flag) map[string]int64 { + variantMap := make(map[string]int64) + + // add variants + for _, variantModel := range flagModel.Variants { + a, _ := r2e.MapAttachment(variantModel.Attachment) + variant := entity.Variant{ + Key: util.SafeString(variantModel.Key), + Attachment: a, + } + getDB().Model(flag).Association("Variants").Append(&variant) + variantMap[util.SafeString(variant.Key)] = int64(variant.ID) + } + return variantMap +} + +func addTags(flagModel models.Flag, flag *entity.Flag) { + // add tags + for _, tagModel := range flagModel.Tags { + t := &entity.Tag{} + t.Value = util.SafeString(tagModel.Value) + getDB().Where("value = ?", util.SafeString(tagModel.Value)).Find(t) + getDB().Model(flag).Association("Tags").Append(t) + } +} + +func deleteSegments(flag *entity.Flag) { + for _, segmentsModel := range flag.Segments { + getDB().Select("Constraints", "Distributions").Delete(&entity.Segment{}, segmentsModel.ID) + } +} + +func deleteVariants(flag *entity.Flag) { + for _, variantsModel := range flag.Variants { + v := &entity.Variant{} + v.ID = variantsModel.ID + getDB().Delete(&entity.Variant{}, variantsModel.ID) + } +} + +func deleteTags(flag *entity.Flag) { + for _, tagsModel := range flag.Tags { + t := &entity.Tag{} + t.ID = uint(tagsModel.ID) + getDB().Model(flag).Association("Tags").Delete(t) + } +} + +func readMigrationFile(dir string, fileName string) ([]models.Flag, error) { + f := dir + string(os.PathSeparator) + fileName + contents, err := os.ReadFile(f) + if err != nil { + return nil, err + } + + var flags []models.Flag + err = yaml.Unmarshal([]byte(contents), &flags) + if err != nil { + return nil, err + } + return flags, nil +} diff --git a/pkg/handler/flag_migrations_test.go b/pkg/handler/flag_migrations_test.go new file mode 100644 index 000000000..08c3a2e50 --- /dev/null +++ b/pkg/handler/flag_migrations_test.go @@ -0,0 +1,31 @@ +package handler + +import ( + "github.com/openflagr/flagr/pkg/entity" + "github.com/prashantv/gostub" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFlagMigrations(t *testing.T) { + db := entity.NewTestDB() + + tmpDB, dbErr := db.DB() + if dbErr != nil { + t.Errorf("Failed to get database") + } + + defer tmpDB.Close() + + defer gostub.StubFunc(&getDB, db).Reset() + + t.Run("FlagMigrationWithValidPath", func(t *testing.T) { + _, err := FlagMigrations("./testdata/migrations") + assert.Nil(t, err) + }) + + t.Run("FlagMigrationWithInvalidPath", func(t *testing.T) { + _, err := FlagMigrations("./testdata/migration") + assert.NotNil(t, err) + }) +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 43bbfbd1c..bed8d13cc 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -12,6 +12,7 @@ import ( "github.com/openflagr/flagr/swagger_gen/restapi/operations/export" "github.com/openflagr/flagr/swagger_gen/restapi/operations/flag" "github.com/openflagr/flagr/swagger_gen/restapi/operations/health" + "github.com/openflagr/flagr/swagger_gen/restapi/operations/migration" "github.com/openflagr/flagr/swagger_gen/restapi/operations/segment" "github.com/openflagr/flagr/swagger_gen/restapi/operations/tag" "github.com/openflagr/flagr/swagger_gen/restapi/operations/variant" @@ -27,10 +28,12 @@ func Setup(api *operations.FlagrAPI) { return } + setupHealth(api) setupEvaluation(api) setupCRUD(api) setupExport(api) + setupMigration(api) } func setupCRUD(api *operations.FlagrAPI) { @@ -102,3 +105,7 @@ func setupExport(api *operations.FlagrAPI) { api.ExportGetExportSqliteHandler = export.GetExportSqliteHandlerFunc(exportSQLiteHandler) api.ExportGetExportEvalCacheJSONHandler = export.GetExportEvalCacheJSONHandlerFunc(exportEvalCacheJSONHandler) } + +func setupMigration(api *operations.FlagrAPI) { + api.MigrationRunMigrationsHandler = migration.RunMigrationsHandlerFunc(runMigrationsHandler) +} diff --git a/pkg/handler/testdata/migrations/test-migration-broken.yaml b/pkg/handler/testdata/migrations/test-migration-broken.yaml new file mode 100644 index 000000000..4bfac2958 --- /dev/null +++ b/pkg/handler/testdata/migrations/test-migration-broken.yaml @@ -0,0 +1,7 @@ +--- +# this is a broken yaml since enabled should be a boolean +- key: FLAG-111 + description: basic flag + entityType: User + dataRecordsEnabled: false + enabled: 9 diff --git a/pkg/handler/testdata/migrations/test-migration.yaml b/pkg/handler/testdata/migrations/test-migration.yaml new file mode 100644 index 000000000..cbc271e4a --- /dev/null +++ b/pkg/handler/testdata/migrations/test-migration.yaml @@ -0,0 +1,43 @@ +--- +# this is a basic +- key: FLAGS-123 + description: a flag added by migration + segments: + - description: just email + rank: 0 + rolloutPercent: 100 + constraints: + - property: email + operator: EQ + value: '"me@me.com"' + distributions: + - variantKey: "on" + percent: 100 + - rank: 1 + rolloutPercent: 100 + distributions: + - variantKey: "off" + percent: 100 + variants: + - key: "off" + - key: "on" + entityType: User + dataRecordsEnabled: true + enabled: false + tags: + - value: "better tags" +- key: FLAGS-123 + description: updated second time + segments: + - rank: 1 + rolloutPercent: 100 + distributions: + - variantKey: "off" + percent: 100 + variants: + - key: "off" + entityType: User + dataRecordsEnabled: false + enabled: true + tags: + - value: "best tag" diff --git a/pkg/mapper/entity_restapi/r2e/r2e.go b/pkg/mapper/entity_restapi/r2e/r2e.go index a4e169e8f..577b692e5 100644 --- a/pkg/mapper/entity_restapi/r2e/r2e.go +++ b/pkg/mapper/entity_restapi/r2e/r2e.go @@ -29,6 +29,25 @@ func MapDistribution(r *models.Distribution, segmentID uint) entity.Distribution return e } +func MapConstraints(r []*models.Constraint, segmentID uint) []entity.Constraint { + e := make([]entity.Constraint, len(r)) + for i, d := range r { + e[i] = MapConstraint(d, segmentID) + } + return e +} + +// MapDistribution maps distribution +func MapConstraint(r *models.Constraint, segmentID uint) entity.Constraint { + e := entity.Constraint{ + SegmentID: segmentID, + Property: util.SafeString(r.Property), + Operator: util.SafeString(r.Operator), + Value: util.SafeString(r.Value), + } + return e +} + // MapAttachment maps attachment func MapAttachment(a interface{}) (entity.Attachment, error) { e := entity.Attachment{} diff --git a/swagger/index.yaml b/swagger/index.yaml index 1b8efd6f2..d05974130 100644 --- a/swagger/index.yaml +++ b/swagger/index.yaml @@ -87,6 +87,8 @@ paths: $ref: ./evaluation_batch.yaml /health: $ref: ./health.yaml + /migrations: + $ref: ./migrations.yaml /export/sqlite: $ref: ./export_sqlite.yaml /export/eval_cache/json: @@ -587,6 +589,20 @@ definitions: status: type: string + migrationStatus: + type: object + properties: + code: + type: integer + migrations: + type: array + x-omitempty: true + items: + type: string + message: + type: string + + # Default Error error: type: object diff --git a/swagger/migrations.yaml b/swagger/migrations.yaml new file mode 100644 index 000000000..e444ab3af --- /dev/null +++ b/swagger/migrations.yaml @@ -0,0 +1,14 @@ +get: + tags: + - migration + operationId: runMigrations + description: Run migrations + responses: + 200: + description: OK + schema: + $ref: "#/definitions/migrationStatus" + default: + description: migration status with error code + schema: + $ref: "#/definitions/migrationStatus" diff --git a/swagger_gen/models/migration_status.go b/swagger_gen/models/migration_status.go new file mode 100644 index 000000000..3bf3dd2d0 --- /dev/null +++ b/swagger_gen/models/migration_status.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// MigrationStatus migration status +// +// swagger:model migrationStatus +type MigrationStatus struct { + + // code + Code int64 `json:"code,omitempty"` + + // message + Message string `json:"message,omitempty"` + + // migrations + Migrations []string `json:"migrations,omitempty"` +} + +// Validate validates this migration status +func (m *MigrationStatus) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this migration status based on context it is used +func (m *MigrationStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *MigrationStatus) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MigrationStatus) UnmarshalBinary(b []byte) error { + var res MigrationStatus + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/swagger_gen/restapi/configure_flagr.go b/swagger_gen/restapi/configure_flagr.go index f29802cd7..2f4592811 100644 --- a/swagger_gen/restapi/configure_flagr.go +++ b/swagger_gen/restapi/configure_flagr.go @@ -4,9 +4,11 @@ package restapi import ( "crypto/tls" + "github.com/go-openapi/swag" jsoniter "github.com/json-iterator/go" - "net/http" "io" + "net/http" + "os" "github.com/openflagr/flagr/pkg/config" "github.com/openflagr/flagr/pkg/handler" @@ -21,8 +23,19 @@ import ( //go:generate swagger generate server --target ../../swagger_gen --name Flagr --spec ../../docs/api_docs/bundle.yaml +var flagMigrationOptions = handler.FlagMigrationOptions{ + Run: false, + Path: "./migrations", +} + func configureFlags(api *operations.FlagrAPI) { - // api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... } + api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ + swag.CommandLineOptionsGroup{ + ShortDescription: "Startup", + Options: &flagMigrationOptions, + }, + } + } func configureAPI(api *operations.FlagrAPI) http.Handler { @@ -60,6 +73,13 @@ func configureTLS(tlsConfig *tls.Config) { // This function can be called multiple times, depending on the number of serving schemes. // scheme value will be set accordingly: "http", "https" or "unix" func configureServer(s *http.Server, scheme, addr string) { + if flagMigrationOptions.Run { + if _, err:= handler.FlagMigrations(flagMigrationOptions.Path); err != nil { + os.Exit(1) + } else { + os.Exit(0) + } + } } // The middleware configuration is for the handler executors. These do not apply to the swagger.json document. diff --git a/swagger_gen/restapi/embedded_spec.go b/swagger_gen/restapi/embedded_spec.go index 1bee1ea4d..3ecdd10f2 100644 --- a/swagger_gen/restapi/embedded_spec.go +++ b/swagger_gen/restapi/embedded_spec.go @@ -1340,6 +1340,29 @@ func init() { } } }, + "/migrations": { + "get": { + "description": "Run migrations", + "tags": [ + "migration" + ], + "operationId": "runMigrations", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/migrationStatus" + } + }, + "default": { + "description": "migration status with error code", + "schema": { + "$ref": "#/definitions/migrationStatus" + } + } + } + } + }, "/tags": { "get": { "tags": [ @@ -1843,6 +1866,24 @@ func init() { } } }, + "migrationStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "migrations": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + } + } + }, "putDistributionsRequest": { "type": "object", "required": [ @@ -3429,6 +3470,29 @@ func init() { } } }, + "/migrations": { + "get": { + "description": "Run migrations", + "tags": [ + "migration" + ], + "operationId": "runMigrations", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/migrationStatus" + } + }, + "default": { + "description": "migration status with error code", + "schema": { + "$ref": "#/definitions/migrationStatus" + } + } + } + } + }, "/tags": { "get": { "tags": [ @@ -3934,6 +3998,24 @@ func init() { } } }, + "migrationStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "migrations": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + } + } + }, "putDistributionsRequest": { "type": "object", "required": [ diff --git a/swagger_gen/restapi/operations/flagr_api.go b/swagger_gen/restapi/operations/flagr_api.go index 40457e654..4e8c8b63c 100644 --- a/swagger_gen/restapi/operations/flagr_api.go +++ b/swagger_gen/restapi/operations/flagr_api.go @@ -25,6 +25,7 @@ import ( "github.com/openflagr/flagr/swagger_gen/restapi/operations/export" "github.com/openflagr/flagr/swagger_gen/restapi/operations/flag" "github.com/openflagr/flagr/swagger_gen/restapi/operations/health" + "github.com/openflagr/flagr/swagger_gen/restapi/operations/migration" "github.com/openflagr/flagr/swagger_gen/restapi/operations/segment" "github.com/openflagr/flagr/swagger_gen/restapi/operations/tag" "github.com/openflagr/flagr/swagger_gen/restapi/operations/variant" @@ -149,6 +150,9 @@ func NewFlagrAPI(spec *loads.Document) *FlagrAPI { FlagRestoreFlagHandler: flag.RestoreFlagHandlerFunc(func(params flag.RestoreFlagParams) middleware.Responder { return middleware.NotImplemented("operation flag.RestoreFlag has not yet been implemented") }), + MigrationRunMigrationsHandler: migration.RunMigrationsHandlerFunc(func(params migration.RunMigrationsParams) middleware.Responder { + return middleware.NotImplemented("operation migration.RunMigrations has not yet been implemented") + }), FlagSetFlagEnabledHandler: flag.SetFlagEnabledHandlerFunc(func(params flag.SetFlagEnabledParams) middleware.Responder { return middleware.NotImplemented("operation flag.SetFlagEnabled has not yet been implemented") }), @@ -256,6 +260,8 @@ type FlagrAPI struct { VariantPutVariantHandler variant.PutVariantHandler // FlagRestoreFlagHandler sets the operation handler for the restore flag operation FlagRestoreFlagHandler flag.RestoreFlagHandler + // MigrationRunMigrationsHandler sets the operation handler for the run migrations operation + MigrationRunMigrationsHandler migration.RunMigrationsHandler // FlagSetFlagEnabledHandler sets the operation handler for the set flag enabled operation FlagSetFlagEnabledHandler flag.SetFlagEnabledHandler @@ -434,6 +440,9 @@ func (o *FlagrAPI) Validate() error { if o.FlagRestoreFlagHandler == nil { unregistered = append(unregistered, "flag.RestoreFlagHandler") } + if o.MigrationRunMigrationsHandler == nil { + unregistered = append(unregistered, "migration.RunMigrationsHandler") + } if o.FlagSetFlagEnabledHandler == nil { unregistered = append(unregistered, "flag.SetFlagEnabledHandler") } @@ -655,6 +664,10 @@ func (o *FlagrAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/flags/{flagID}/restore"] = flag.NewRestoreFlag(o.context, o.FlagRestoreFlagHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/migrations"] = migration.NewRunMigrations(o.context, o.MigrationRunMigrationsHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/swagger_gen/restapi/operations/migration/run_migrations.go b/swagger_gen/restapi/operations/migration/run_migrations.go new file mode 100644 index 000000000..3d6829855 --- /dev/null +++ b/swagger_gen/restapi/operations/migration/run_migrations.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package migration + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// RunMigrationsHandlerFunc turns a function with the right signature into a run migrations handler +type RunMigrationsHandlerFunc func(RunMigrationsParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn RunMigrationsHandlerFunc) Handle(params RunMigrationsParams) middleware.Responder { + return fn(params) +} + +// RunMigrationsHandler interface for that can handle valid run migrations params +type RunMigrationsHandler interface { + Handle(RunMigrationsParams) middleware.Responder +} + +// NewRunMigrations creates a new http.Handler for the run migrations operation +func NewRunMigrations(ctx *middleware.Context, handler RunMigrationsHandler) *RunMigrations { + return &RunMigrations{Context: ctx, Handler: handler} +} + +/* + RunMigrations swagger:route GET /migrations migration runMigrations + +Run migrations +*/ +type RunMigrations struct { + Context *middleware.Context + Handler RunMigrationsHandler +} + +func (o *RunMigrations) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewRunMigrationsParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/swagger_gen/restapi/operations/migration/run_migrations_parameters.go b/swagger_gen/restapi/operations/migration/run_migrations_parameters.go new file mode 100644 index 000000000..694e7be0a --- /dev/null +++ b/swagger_gen/restapi/operations/migration/run_migrations_parameters.go @@ -0,0 +1,46 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package migration + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" +) + +// NewRunMigrationsParams creates a new RunMigrationsParams object +// +// There are no default values defined in the spec. +func NewRunMigrationsParams() RunMigrationsParams { + + return RunMigrationsParams{} +} + +// RunMigrationsParams contains all the bound params for the run migrations operation +// typically these are obtained from a http.Request +// +// swagger:parameters runMigrations +type RunMigrationsParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewRunMigrationsParams() beforehand. +func (o *RunMigrationsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/swagger_gen/restapi/operations/migration/run_migrations_responses.go b/swagger_gen/restapi/operations/migration/run_migrations_responses.go new file mode 100644 index 000000000..82d04d378 --- /dev/null +++ b/swagger_gen/restapi/operations/migration/run_migrations_responses.go @@ -0,0 +1,118 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package migration + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/openflagr/flagr/swagger_gen/models" +) + +// RunMigrationsOKCode is the HTTP code returned for type RunMigrationsOK +const RunMigrationsOKCode int = 200 + +/* +RunMigrationsOK OK + +swagger:response runMigrationsOK +*/ +type RunMigrationsOK struct { + + /* + In: Body + */ + Payload *models.MigrationStatus `json:"body,omitempty"` +} + +// NewRunMigrationsOK creates RunMigrationsOK with default headers values +func NewRunMigrationsOK() *RunMigrationsOK { + + return &RunMigrationsOK{} +} + +// WithPayload adds the payload to the run migrations o k response +func (o *RunMigrationsOK) WithPayload(payload *models.MigrationStatus) *RunMigrationsOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the run migrations o k response +func (o *RunMigrationsOK) SetPayload(payload *models.MigrationStatus) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RunMigrationsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/* +RunMigrationsDefault migration status with error code + +swagger:response runMigrationsDefault +*/ +type RunMigrationsDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.MigrationStatus `json:"body,omitempty"` +} + +// NewRunMigrationsDefault creates RunMigrationsDefault with default headers values +func NewRunMigrationsDefault(code int) *RunMigrationsDefault { + if code <= 0 { + code = 500 + } + + return &RunMigrationsDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the run migrations default response +func (o *RunMigrationsDefault) WithStatusCode(code int) *RunMigrationsDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the run migrations default response +func (o *RunMigrationsDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the run migrations default response +func (o *RunMigrationsDefault) WithPayload(payload *models.MigrationStatus) *RunMigrationsDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the run migrations default response +func (o *RunMigrationsDefault) SetPayload(payload *models.MigrationStatus) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RunMigrationsDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/swagger_gen/restapi/operations/migration/run_migrations_urlbuilder.go b/swagger_gen/restapi/operations/migration/run_migrations_urlbuilder.go new file mode 100644 index 000000000..e2ea76342 --- /dev/null +++ b/swagger_gen/restapi/operations/migration/run_migrations_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package migration + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// RunMigrationsURL generates an URL for the run migrations operation +type RunMigrationsURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *RunMigrationsURL) WithBasePath(bp string) *RunMigrationsURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *RunMigrationsURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *RunMigrationsURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/migrations" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *RunMigrationsURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *RunMigrationsURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *RunMigrationsURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on RunMigrationsURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on RunMigrationsURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *RunMigrationsURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +}