Skip to content
This repository has been archived by the owner on Mar 24, 2023. It is now read-only.

Commit

Permalink
Merge pull request #994 from divolgin/state-url
Browse files Browse the repository at this point in the history
Support saving/loading state data using an HTTP URL
  • Loading branch information
divolgin authored Jun 24, 2019
2 parents 451506b + 1d230c6 commit 8c70e06
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 161 deletions.
4 changes: 3 additions & 1 deletion pkg/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ func RootCmd() *cobra.Command {
// TODO remove me, just always set this to true
cmd.PersistentFlags().BoolP("navcycle", "", true, "set to false to run ship in v1/non-navigable mode (deprecated)")

cmd.PersistentFlags().String("state-from", "file", "type of resource to use when loading/saving state (currently supported values: 'file', 'secret'")
cmd.PersistentFlags().String("state-from", "file", "type of resource to use when loading/saving state (currently supported values: 'file', 'secret', 'url'")
cmd.PersistentFlags().String("state-file", "", fmt.Sprintf("path to the state file to read from, defaults to %s", constants.StatePath))
cmd.PersistentFlags().String("secret-namespace", "default", "namespace containing the state secret")
cmd.PersistentFlags().String("secret-name", "", "name of the secret to load state from")
cmd.PersistentFlags().String("secret-key", "", "name of the key in the secret containing state")
cmd.PersistentFlags().String("state-put-url", "", "the URL that will be used to store update state")
cmd.PersistentFlags().String("state-get-url", "", "the URL that will be used to retrieve update state")

cmd.PersistentFlags().String("upload-assets-to", "", "URL to upload assets to via HTTP PUT request. NOTE: this will cause the entire working directory to be uploaded to the specified URL, use with caution.")

Expand Down
193 changes: 33 additions & 160 deletions pkg/state/manager.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package state

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"

"github.com/go-kit/kit/log"
Expand All @@ -19,9 +15,6 @@ import (

"github.com/spf13/afero"
"github.com/spf13/viper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

type Manager interface {
Expand Down Expand Up @@ -50,6 +43,11 @@ type Manager interface {
AddCA(name string, newCA util.CAType) error
}

type stateSerializer interface {
Load() (State, error)
Save(State) error
}

var _ Manager = &MManager{}

// MManager is the saved output of a plan run to load on future runs
Expand Down Expand Up @@ -241,118 +239,54 @@ func (m *MManager) SerializeUpstreamContents(contents *UpstreamContents) error {
return err
}

// TryLoad will attempt to load a state file from disk, if present
func (m *MManager) TryLoad() (State, error) {
m.StateRWMut.RLock()
defer m.StateRWMut.RUnlock()
func (m *MManager) getStateSerializer() (stateSerializer, error) {
stateFrom := m.V.GetString("state-from")
if stateFrom == "" {
stateFrom = "file"
}

// TODO consider an interface

switch stateFrom {
case "file":
return m.tryLoadFromFile()
return newFileSerializer(m.FS, m.Logger), nil
case "secret":
return m.tryLoadFromSecret()
return newSecretSerializer(m.Logger, m.V.GetString("secret-namespace"), m.V.GetString("secret-name"), m.V.GetString("secret-key")), nil
case "url":
return newURLSerializer(m.Logger, m.V.GetString("state-get-url"), m.V.GetString("state-put-url")), nil
default:
err := fmt.Errorf("unsupported state-from value: %q", stateFrom)
return State{}, errors.Wrap(err, "try load state")
return nil, fmt.Errorf("unsupported state-from value: %q", stateFrom)
}
}

// ResetLifecycle is used by `ship update --headed` to reset the saved stepsCompleted
// in the state.json
func (m *MManager) ResetLifecycle() error {
debug := level.Debug(log.With(m.Logger, "method", "ResetLifecycle"))

debug.Log("event", "safeStateUpdate")
_, err := m.StateUpdate(func(state State) (State, error) {

state.V1.Lifecycle = nil
return state, nil
})
return err
}

// tryLoadFromSecret will attempt to load the state from a secret
// currently only supports in-cluster execution
func (m *MManager) tryLoadFromSecret() (State, error) {
config, err := rest.InClusterConfig()
if err != nil {
return State{}, errors.Wrap(err, "get in cluster config")
}
// TryLoad will attempt to load a state file from disk, if present
func (m *MManager) TryLoad() (State, error) {
m.StateRWMut.RLock()
defer m.StateRWMut.RUnlock()

clientset, err := kubernetes.NewForConfig(config)
s, err := m.getStateSerializer()
if err != nil {
return State{}, errors.Wrap(err, "get kubernetes client")
}

ns := m.V.GetString("secret-namespace")
if ns == "" {
return State{}, errors.New("secret-namespace is not set")
}
secretName := m.V.GetString("secret-name")
if secretName == "" {
return State{}, errors.New("secret-name is not set")
}
secretKey := m.V.GetString("secret-key")
if secretKey == "" {
return State{}, errors.New("secret-key is not set")
return State{}, errors.Wrap(err, "create state serializer")
}

secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
state, err := s.Load()
if err != nil {
return State{}, errors.Wrap(err, "get secret")
}

serialized, ok := secret.Data[secretKey]
if !ok {
err := fmt.Errorf("key %q not found in secret %q", secretKey, secretName)
return State{}, errors.Wrap(err, "get state from secret")
return State{}, errors.Wrap(err, "load state")
}

// An empty secret should be treated as empty state
if len(strings.TrimSpace(string(serialized))) == 0 {
return State{}, nil
}

var state State
if err := json.Unmarshal(serialized, &state); err != nil {
return State{}, errors.Wrap(err, "unmarshal state")
}

level.Debug(m.Logger).Log(
"event", "state.unmarshal",
"type", "versioned",
"source", "secret",
"value", fmt.Sprintf("%+v", state),
)

level.Debug(m.Logger).Log("event", "state.resolve", "type", "versioned")
return state, nil
}

func (m *MManager) tryLoadFromFile() (State, error) {
if _, err := m.FS.Stat(constants.StatePath); os.IsNotExist(err) {
level.Debug(m.Logger).Log("msg", "no saved state exists", "path", constants.StatePath)
return State{}, nil
}

serialized, err := m.FS.ReadFile(constants.StatePath)
if err != nil {
return State{}, errors.Wrap(err, "read state file")
}
// ResetLifecycle is used by `ship update --headed` to reset the saved stepsCompleted
// in the state.json
func (m *MManager) ResetLifecycle() error {
debug := level.Debug(log.With(m.Logger, "method", "ResetLifecycle"))

var state State
if err := json.Unmarshal(serialized, &state); err != nil {
return State{}, errors.Wrap(err, "unmarshal state")
}
debug.Log("event", "safeStateUpdate")
_, err := m.StateUpdate(func(state State) (State, error) {

level.Debug(m.Logger).Log("event", "state.resolve", "type", "versioned")
return state, nil
state.V1.Lifecycle = nil
return state, nil
})
return err
}

func (m *MManager) SaveKustomize(kustomize *Kustomize) error {
Expand Down Expand Up @@ -385,76 +319,15 @@ func (m *MManager) RemoveStateFile() error {
func (m *MManager) serializeAndWriteState(state State) error {
m.StateRWMut.Lock()
defer m.StateRWMut.Unlock()
debug := level.Debug(log.With(m.Logger, "method", "serializeAndWriteState"))
state = state.migrateDeprecatedFields()

stateFrom := m.V.GetString("state-from")
if stateFrom == "" {
stateFrom = "file"
}

debug.Log("stateFrom", stateFrom)

switch stateFrom {
case "file":
return m.serializeAndWriteStateFile(state)
case "secret":
return m.serializeAndWriteStateSecret(state)
default:
err := fmt.Errorf("unsupported state-from value: %q", stateFrom)
return errors.Wrap(err, "serializeAndWriteState")
}
}

func (m *MManager) serializeAndWriteStateFile(state State) error {

serialized, err := json.MarshalIndent(state, "", " ")
if err != nil {
return errors.Wrap(err, "serialize state")
}

err = m.FS.MkdirAll(filepath.Dir(constants.StatePath), 0700)
if err != nil {
return errors.Wrap(err, "mkdir state")
}

err = m.FS.WriteFile(constants.StatePath, serialized, 0644)
if err != nil {
return errors.Wrap(err, "write state file")
}

return nil
}

func (m *MManager) serializeAndWriteStateSecret(state State) error {
serialized, err := json.MarshalIndent(state, "", " ")
s, err := m.getStateSerializer()
if err != nil {
return errors.Wrap(err, "serialize state")
return errors.Wrap(err, "create state serializer")
}

config, err := rest.InClusterConfig()
if err != nil {
return errors.Wrap(err, "get in cluster config")
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return errors.Wrap(err, "get kubernetes client")
}

secret, err := clientset.CoreV1().Secrets(m.V.GetString("secret-namespace")).Get(m.V.GetString("secret-name"), metav1.GetOptions{})
if err != nil {
return errors.Wrap(err, "get secret")
}

secret.Data[m.V.GetString("secret-key")] = serialized
debug := level.Debug(log.With(m.Logger, "method", "serializeHelmValues"))

debug.Log("event", "serializeAndWriteStateSecret", "name", secret.Name, "key", m.V.GetString("secret-key"))

_, err = clientset.CoreV1().Secrets(m.V.GetString("secret-namespace")).Update(secret)
if err != nil {
return errors.Wrap(err, "update secret")
if err := s.Save(state); err != nil {
return errors.Wrap(err, "save state")
}

return nil
Expand Down
61 changes: 61 additions & 0 deletions pkg/state/state_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package state

import (
"encoding/json"
"os"
"path/filepath"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/pkg/errors"
"github.com/replicatedhq/ship/pkg/constants"
"github.com/spf13/afero"
)

type fileSerializer struct {
fs afero.Afero
logger log.Logger
}

func newFileSerializer(fs afero.Afero, logger log.Logger) stateSerializer {
return &fileSerializer{fs: fs, logger: logger}
}

func (s *fileSerializer) Load() (State, error) {
if _, err := s.fs.Stat(constants.StatePath); os.IsNotExist(err) {
level.Debug(s.logger).Log("msg", "no saved state exists", "path", constants.StatePath)
return State{}, nil
}

serialized, err := s.fs.ReadFile(constants.StatePath)
if err != nil {
return State{}, errors.Wrap(err, "read state file")
}

var state State
if err := json.Unmarshal(serialized, &state); err != nil {
return State{}, errors.Wrap(err, "unmarshal state")
}

level.Debug(s.logger).Log("event", "state.resolve", "type", "versioned")
return state, nil
}

func (s *fileSerializer) Save(state State) error {
serialized, err := json.MarshalIndent(state, "", " ")
if err != nil {
return errors.Wrap(err, "serialize state")
}

err = s.fs.MkdirAll(filepath.Dir(constants.StatePath), 0700)
if err != nil {
return errors.Wrap(err, "mkdir state")
}

err = s.fs.WriteFile(constants.StatePath, serialized, 0644)
if err != nil {
return errors.Wrap(err, "write state file")
}

return nil
}
Loading

0 comments on commit 8c70e06

Please sign in to comment.