Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions cmd/service/rotate_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright (c) Tobias Schäfer. All rights reserved.
Licensed under the MIT license, see LICENSE in the project root for details.
*/
package service

import (
"github.com/spf13/cobra"
"github.com/tschaefer/finchctl/cmd/completion"
"github.com/tschaefer/finchctl/cmd/errors"
"github.com/tschaefer/finchctl/cmd/format"
"github.com/tschaefer/finchctl/internal/service"
)

var rotateSecretCmd = &cobra.Command{
Use: "rotate-secret [user@]host[:port]",
Short: "Rotate secret of a service on a remote host",
Args: cobra.ExactArgs(1),
Run: runRotateSecretCmd,
ValidArgsFunction: completion.CompleteStackName,
}

func init() {
rotateSecretCmd.Flags().String("run.format", "progress", "output format")
rotateSecretCmd.Flags().Bool("run.dry-run", false, "do not rotate secret, just print the commands that would be run")

_ = rotateSecretCmd.RegisterFlagCompletionFunc("run.format", completion.CompleteRunFormat)
}

func runRotateSecretCmd(cmd *cobra.Command, args []string) {
targetUrl := args[0]

formatName, _ := cmd.Flags().GetString("run.format")
formatType, err := format.GetRunFormat(formatName)
cobra.CheckErr(err)
dryRun, _ := cmd.Flags().GetBool("run.dry-run")

s, err := service.New(nil, targetUrl, formatType, dryRun)
errors.CheckErr(err, formatType)

err = s.RotateSecret()
errors.CheckErr(err, formatType)
}
1 change: 1 addition & 0 deletions cmd/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ func init() {
Cmd.AddCommand(teardownCmd)
Cmd.AddCommand(infoCmd)
Cmd.AddCommand(dashboardCmd)
Cmd.AddCommand(rotateSecretCmd)
}
4 changes: 2 additions & 2 deletions internal/service/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ func (s *Service) __deployCopyFinchConfig() error {
Database: "sqlite://finch.db",
Profiler: "http://pyroscope:4040",
Secret: secret,
Version: "1.3.0",
Version: "1.7.0",
}

return s.__helperCopyTemplate(path, "400", "10002:10002", data)
return s.__helperCopyTemplate(path, "400", "0:0", data)
}

func (s *Service) __deployCopyGrafanaDashboards() error {
Expand Down
9 changes: 9 additions & 0 deletions internal/service/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ func (e *InfoServiceError) Error() string {
return strings.TrimSpace(fmt.Sprintf("Failed to get service info: %s %s", e.Message, e.Reason))
}

type RotateServiceSecretError struct {
Message string
Reason string
}

func (e *RotateServiceSecretError) Error() string {
return strings.TrimSpace(fmt.Sprintf("Failed to rotate service secret: %s %s", e.Message, e.Reason))
}

func convertError(err error, to any) error {
if err == nil {
return nil
Expand Down
47 changes: 47 additions & 0 deletions internal/service/rotate_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright (c) Tobias Schäfer. All rights reserved.
Licensed under the MIT license, see LICENSE in the project root for details.
*/
package service

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
)

func (s *Service) rotateServiceSecret() error {
cfgPath := fmt.Sprintf("%s/finch.json", s.libDir())
out, err := s.target.Run(fmt.Sprintf("sudo cat %s", cfgPath))
if err != nil {
return &RotateServiceSecretError{Message: err.Error(), Reason: string(out)}
}
if s.dryRun {
out = []byte(`{}`)
}

cfg := FinchConfig{}
err = json.Unmarshal(out, &cfg)
if err != nil {
return &RotateServiceSecretError{Message: err.Error(), Reason: string(out)}
}

key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return &RotateServiceSecretError{Message: err.Error(), Reason: ""}
}
secret := base64.StdEncoding.EncodeToString(key)
cfg.Secret = secret

if err := s.__helperCopyTemplate(cfgPath, "400", "0:0", cfg); err != nil {
return convertError(err, &RotateServiceSecretError{})
}

out, err = s.target.Run(fmt.Sprintf("sudo docker compose --file %s/docker-compose.yaml restart finch", s.libDir()))
if err != nil {
return &RotateServiceSecretError{Message: err.Error(), Reason: string(out)}
}

return nil
}
18 changes: 18 additions & 0 deletions internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,21 @@ func (s *Service) libDir() string {

return dir
}

func (s *Service) RotateSecret() error {
defer func() {
if s.format == target.FormatProgress {
println()
}
}()

if err := s.requirementsService(); err != nil {
return convertError(err, &RotateServiceSecretError{})
}

if err := s.rotateServiceSecret(); err != nil {
return err
}

return nil
}
35 changes: 35 additions & 0 deletions internal/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,41 @@ func Test_Update(t *testing.T) {
assert.NotEmpty(t, track.Timestamp, "first log line timestamp")
}

func Test_RotateSecret(t *testing.T) {
s, err := New(nil, "localhost", target.FormatDocumentation, true)
assert.NoError(t, err, "create service")

record := capture(func() {
err = s.RotateSecret()
})
assert.NoError(t, err, "rotate secret")

tracks := strings.Split(record, "\n")
assert.Len(t, tracks, 7, "number of log lines mismatch")

wanted := "Running 'command -v sudo' as .+@localhost"
assert.Regexp(t, wanted, tracks[0], "first log line")

wanted = "Running 'sudo docker compose --file .+/docker-compose.yaml restart finch' as .+@localhost"
assert.Regexp(t, wanted, tracks[len(tracks)-2], "last log line")

s, err = New(nil, "localhost", target.FormatJSON, true)
assert.NoError(t, err, "create service")

record = capture(func() {
err = s.RotateSecret()
})
assert.NoError(t, err, "rotate secret")

tracks = strings.Split(record, "\n")
var track track
err = json.Unmarshal([]byte(tracks[0]), &track)
assert.NoError(t, err, "unmarshal json output")

wanted = "Running 'command -v sudo' as .+@localhost"
assert.Regexp(t, wanted, tracks[0], "first log line")
}

func capture(f func()) string {
originalStdout := os.Stdout

Expand Down