diff --git a/README.md b/README.md index adc2f0b..59a6190 100644 --- a/README.md +++ b/README.md @@ -265,17 +265,24 @@ Usage: lambroll rollback rollback function Flags: - --dry-run dry run - --delete-version delete rolled back version + --dry-run dry run + --alias="current" alias to rollback + --version="" version to rollback (default: previous version auto detected) + --delete-version delete rolled back version ``` -`lambroll deploy` create/update alias `current` to the published function version on deploy. +`lambroll deploy` create/update alias to the published function version on deploy. `lambroll rollback` works as below. -1. Find previous one version of function. -2. Update alias `current` to the previous version. -3. When `--delete-version` specified, delete old version of function. +1. Find the previous version from the alias with no other aliases. +2. Update the alias to the previous version. + - If `--version` is specified, update the alias to the specified version. +3. When `--delete-version` is specified, delete the old version of the function. + +If you add multiple aliases to the function, `lambroll rollback --alias={some-alias}` may not work as expected. Because the previous version that auto-detected may be the older version of other aliases. + +So you should specify the version to rollback with `--version` flag to clear the ambiguity. ### Invoke diff --git a/rollback.go b/rollback.go index bd96aa0..f2a84c7 100644 --- a/rollback.go +++ b/rollback.go @@ -15,8 +15,10 @@ import ( // RollbackOption represents option for Rollback() type RollbackOption struct { - DryRun bool `default:"false" help:"dry run"` - DeleteVersion bool `default:"false" help:"delete rolled back version"` + DryRun bool `default:"false" help:"dry run"` + Alias string `default:"current" help:"alias to rollback"` + Version string `default:"" help:"version to rollback (default: previous version auto detected)"` + DeleteVersion bool `default:"false" help:"delete rolled back version"` } func (opt RollbackOption) label() string { @@ -33,20 +35,51 @@ func (app *App) Rollback(ctx context.Context, opt *RollbackOption) error { return fmt.Errorf("failed to load function: %w", err) } - log.Printf("[info] starting rollback function %s", *fn.FunctionName) + log.Printf("[info] starting rollback function %s:%s", *fn.FunctionName, opt.Alias) res, err := app.lambda.GetAlias(ctx, &lambda.GetAliasInput{ FunctionName: fn.FunctionName, - Name: aws.String(CurrentAliasName), + Name: aws.String(opt.Alias), }) if err != nil { return fmt.Errorf("failed to get alias: %w", err) } currentVersion := *res.FunctionVersion + var prevVersion string + if opt.Version != "" { + prevVersion = opt.Version + } else { + prevVersion, err = app.findPreviousVersion(ctx, *fn.FunctionName, currentVersion) + if err != nil { + return fmt.Errorf("failed to find previous version: %w", err) + } + } + + log.Printf("[info] rollbacking function version %s to %s %s", currentVersion, prevVersion, opt.label()) + if opt.DryRun { + return nil + } + err = app.updateAliases(ctx, *fn.FunctionName, versionAlias{Version: prevVersion, Name: opt.Alias}) + if err != nil { + return err + } + + if !opt.DeleteVersion { + return nil + } + + return app.deleteFunctionVersion(ctx, *fn.FunctionName, currentVersion) +} + +func (app *App) findPreviousVersion(ctx context.Context, name, currentVersion string) (string, error) { + aliases, err := app.getAliases(ctx, name) + if err != nil { + return "", fmt.Errorf("failed to get aliases: %w", err) + } cv, err := strconv.ParseInt(currentVersion, 10, 64) if err != nil { - return fmt.Errorf("failed to pase %s as int: %w", currentVersion, err) + return "", fmt.Errorf("failed to pase %s as int: %w", currentVersion, err) } var prevVersion string @@ -55,7 +88,7 @@ VERSIONS: log.Printf("[debug] get function version %d", v) vs := strconv.FormatInt(v, 10) res, err := app.lambda.GetFunction(ctx, &lambda.GetFunctionInput{ - FunctionName: fn.FunctionName, + FunctionName: aws.String(name), Qualifier: aws.String(vs), }) if err != nil { @@ -64,30 +97,21 @@ VERSIONS: log.Printf("[debug] version %s not found", vs) continue VERSIONS } else { - return fmt.Errorf("failed to get function: %w", err) + return "", fmt.Errorf("failed to get function: %w", err) } } + if pv := *res.Configuration.Version; aliases[pv] != nil { + // skip if the version has alias + log.Printf("[info] version %s has alias %v, skipping", pv, aliases[pv]) + continue VERSIONS + } prevVersion = *res.Configuration.Version break } if prevVersion == "" { - return errors.New("unable to detect previous version of function") - } - - log.Printf("[info] rollbacking function version %s to %s %s", currentVersion, prevVersion, opt.label()) - if opt.DryRun { - return nil + return "", fmt.Errorf("unable to detect previous version of function") } - err = app.updateAliases(ctx, *fn.FunctionName, versionAlias{Version: prevVersion, Name: CurrentAliasName}) - if err != nil { - return err - } - - if !opt.DeleteVersion { - return nil - } - - return app.deleteFunctionVersion(ctx, *fn.FunctionName, currentVersion) + return prevVersion, nil } func (app *App) deleteFunctionVersion(ctx context.Context, functionName, version string) error { diff --git a/versions.go b/versions.go index fe8fc0d..bebfb7b 100644 --- a/versions.go +++ b/versions.go @@ -82,28 +82,9 @@ func (app *App) Versions(ctx context.Context, opt *VersionsOption) error { return app.deleteVersions(ctx, name, opt.KeepVersions) } - aliases := make(map[string][]string) - var nextAliasMarker *string - for { - res, err := app.lambda.ListAliases(ctx, &lambda.ListAliasesInput{ - FunctionName: &name, - Marker: nextAliasMarker, - }) - if err != nil { - return fmt.Errorf("failed to list aliases: %w", err) - } - for _, alias := range res.Aliases { - aliases[*alias.FunctionVersion] = append(aliases[*alias.FunctionVersion], *alias.Name) - if alias.RoutingConfig == nil || alias.RoutingConfig.AdditionalVersionWeights == nil { - continue - } - for v := range alias.RoutingConfig.AdditionalVersionWeights { - aliases[v] = append(aliases[v], *alias.Name) - } - } - if nextAliasMarker = res.NextMarker; nextAliasMarker == nil { - break - } + aliases, err := app.getAliases(ctx, name) + if err != nil { + return fmt.Errorf("failed to get aliases: %w", err) } var versions []types.FunctionConfiguration @@ -158,3 +139,30 @@ func (app *App) Versions(ctx context.Context, opt *VersionsOption) error { } return nil } + +func (app *App) getAliases(ctx context.Context, name string) (map[string][]string, error) { + aliases := make(map[string][]string) + var nextAliasMarker *string + for { + res, err := app.lambda.ListAliases(ctx, &lambda.ListAliasesInput{ + FunctionName: &name, + Marker: nextAliasMarker, + }) + if err != nil { + return nil, fmt.Errorf("failed to list aliases: %w", err) + } + for _, alias := range res.Aliases { + aliases[*alias.FunctionVersion] = append(aliases[*alias.FunctionVersion], *alias.Name) + if alias.RoutingConfig == nil || alias.RoutingConfig.AdditionalVersionWeights == nil { + continue + } + for v := range alias.RoutingConfig.AdditionalVersionWeights { + aliases[v] = append(aliases[v], *alias.Name) + } + } + if nextAliasMarker = res.NextMarker; nextAliasMarker == nil { + break + } + } + return aliases, nil +}