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

Commit 8c70e06

Browse files
authored
Merge pull request #994 from divolgin/state-url
Support saving/loading state data using an HTTP URL
2 parents 451506b + 1d230c6 commit 8c70e06

File tree

6 files changed

+382
-161
lines changed

6 files changed

+382
-161
lines changed

pkg/cli/root.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ func RootCmd() *cobra.Command {
5656
// TODO remove me, just always set this to true
5757
cmd.PersistentFlags().BoolP("navcycle", "", true, "set to false to run ship in v1/non-navigable mode (deprecated)")
5858

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

6567
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.")
6668

pkg/state/manager.go

+33-160
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package state
22

33
import (
4-
"encoding/json"
54
"fmt"
6-
"os"
7-
"path/filepath"
8-
"strings"
95
"sync"
106

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

2016
"github.com/spf13/afero"
2117
"github.com/spf13/viper"
22-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23-
"k8s.io/client-go/kubernetes"
24-
"k8s.io/client-go/rest"
2518
)
2619

2720
type Manager interface {
@@ -50,6 +43,11 @@ type Manager interface {
5043
AddCA(name string, newCA util.CAType) error
5144
}
5245

46+
type stateSerializer interface {
47+
Load() (State, error)
48+
Save(State) error
49+
}
50+
5351
var _ Manager = &MManager{}
5452

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

244-
// TryLoad will attempt to load a state file from disk, if present
245-
func (m *MManager) TryLoad() (State, error) {
246-
m.StateRWMut.RLock()
247-
defer m.StateRWMut.RUnlock()
242+
func (m *MManager) getStateSerializer() (stateSerializer, error) {
248243
stateFrom := m.V.GetString("state-from")
249244
if stateFrom == "" {
250245
stateFrom = "file"
251246
}
252247

253-
// TODO consider an interface
254-
255248
switch stateFrom {
256249
case "file":
257-
return m.tryLoadFromFile()
250+
return newFileSerializer(m.FS, m.Logger), nil
258251
case "secret":
259-
return m.tryLoadFromSecret()
252+
return newSecretSerializer(m.Logger, m.V.GetString("secret-namespace"), m.V.GetString("secret-name"), m.V.GetString("secret-key")), nil
253+
case "url":
254+
return newURLSerializer(m.Logger, m.V.GetString("state-get-url"), m.V.GetString("state-put-url")), nil
260255
default:
261-
err := fmt.Errorf("unsupported state-from value: %q", stateFrom)
262-
return State{}, errors.Wrap(err, "try load state")
256+
return nil, fmt.Errorf("unsupported state-from value: %q", stateFrom)
263257
}
264258
}
265259

266-
// ResetLifecycle is used by `ship update --headed` to reset the saved stepsCompleted
267-
// in the state.json
268-
func (m *MManager) ResetLifecycle() error {
269-
debug := level.Debug(log.With(m.Logger, "method", "ResetLifecycle"))
270-
271-
debug.Log("event", "safeStateUpdate")
272-
_, err := m.StateUpdate(func(state State) (State, error) {
273-
274-
state.V1.Lifecycle = nil
275-
return state, nil
276-
})
277-
return err
278-
}
279-
280-
// tryLoadFromSecret will attempt to load the state from a secret
281-
// currently only supports in-cluster execution
282-
func (m *MManager) tryLoadFromSecret() (State, error) {
283-
config, err := rest.InClusterConfig()
284-
if err != nil {
285-
return State{}, errors.Wrap(err, "get in cluster config")
286-
}
260+
// TryLoad will attempt to load a state file from disk, if present
261+
func (m *MManager) TryLoad() (State, error) {
262+
m.StateRWMut.RLock()
263+
defer m.StateRWMut.RUnlock()
287264

288-
clientset, err := kubernetes.NewForConfig(config)
265+
s, err := m.getStateSerializer()
289266
if err != nil {
290-
return State{}, errors.Wrap(err, "get kubernetes client")
291-
}
292-
293-
ns := m.V.GetString("secret-namespace")
294-
if ns == "" {
295-
return State{}, errors.New("secret-namespace is not set")
296-
}
297-
secretName := m.V.GetString("secret-name")
298-
if secretName == "" {
299-
return State{}, errors.New("secret-name is not set")
300-
}
301-
secretKey := m.V.GetString("secret-key")
302-
if secretKey == "" {
303-
return State{}, errors.New("secret-key is not set")
267+
return State{}, errors.Wrap(err, "create state serializer")
304268
}
305269

306-
secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
270+
state, err := s.Load()
307271
if err != nil {
308-
return State{}, errors.Wrap(err, "get secret")
309-
}
310-
311-
serialized, ok := secret.Data[secretKey]
312-
if !ok {
313-
err := fmt.Errorf("key %q not found in secret %q", secretKey, secretName)
314-
return State{}, errors.Wrap(err, "get state from secret")
272+
return State{}, errors.Wrap(err, "load state")
315273
}
316274

317-
// An empty secret should be treated as empty state
318-
if len(strings.TrimSpace(string(serialized))) == 0 {
319-
return State{}, nil
320-
}
321-
322-
var state State
323-
if err := json.Unmarshal(serialized, &state); err != nil {
324-
return State{}, errors.Wrap(err, "unmarshal state")
325-
}
326-
327-
level.Debug(m.Logger).Log(
328-
"event", "state.unmarshal",
329-
"type", "versioned",
330-
"source", "secret",
331-
"value", fmt.Sprintf("%+v", state),
332-
)
333-
334-
level.Debug(m.Logger).Log("event", "state.resolve", "type", "versioned")
335275
return state, nil
336276
}
337277

338-
func (m *MManager) tryLoadFromFile() (State, error) {
339-
if _, err := m.FS.Stat(constants.StatePath); os.IsNotExist(err) {
340-
level.Debug(m.Logger).Log("msg", "no saved state exists", "path", constants.StatePath)
341-
return State{}, nil
342-
}
343-
344-
serialized, err := m.FS.ReadFile(constants.StatePath)
345-
if err != nil {
346-
return State{}, errors.Wrap(err, "read state file")
347-
}
278+
// ResetLifecycle is used by `ship update --headed` to reset the saved stepsCompleted
279+
// in the state.json
280+
func (m *MManager) ResetLifecycle() error {
281+
debug := level.Debug(log.With(m.Logger, "method", "ResetLifecycle"))
348282

349-
var state State
350-
if err := json.Unmarshal(serialized, &state); err != nil {
351-
return State{}, errors.Wrap(err, "unmarshal state")
352-
}
283+
debug.Log("event", "safeStateUpdate")
284+
_, err := m.StateUpdate(func(state State) (State, error) {
353285

354-
level.Debug(m.Logger).Log("event", "state.resolve", "type", "versioned")
355-
return state, nil
286+
state.V1.Lifecycle = nil
287+
return state, nil
288+
})
289+
return err
356290
}
357291

358292
func (m *MManager) SaveKustomize(kustomize *Kustomize) error {
@@ -385,76 +319,15 @@ func (m *MManager) RemoveStateFile() error {
385319
func (m *MManager) serializeAndWriteState(state State) error {
386320
m.StateRWMut.Lock()
387321
defer m.StateRWMut.Unlock()
388-
debug := level.Debug(log.With(m.Logger, "method", "serializeAndWriteState"))
389322
state = state.migrateDeprecatedFields()
390323

391-
stateFrom := m.V.GetString("state-from")
392-
if stateFrom == "" {
393-
stateFrom = "file"
394-
}
395-
396-
debug.Log("stateFrom", stateFrom)
397-
398-
switch stateFrom {
399-
case "file":
400-
return m.serializeAndWriteStateFile(state)
401-
case "secret":
402-
return m.serializeAndWriteStateSecret(state)
403-
default:
404-
err := fmt.Errorf("unsupported state-from value: %q", stateFrom)
405-
return errors.Wrap(err, "serializeAndWriteState")
406-
}
407-
}
408-
409-
func (m *MManager) serializeAndWriteStateFile(state State) error {
410-
411-
serialized, err := json.MarshalIndent(state, "", " ")
412-
if err != nil {
413-
return errors.Wrap(err, "serialize state")
414-
}
415-
416-
err = m.FS.MkdirAll(filepath.Dir(constants.StatePath), 0700)
417-
if err != nil {
418-
return errors.Wrap(err, "mkdir state")
419-
}
420-
421-
err = m.FS.WriteFile(constants.StatePath, serialized, 0644)
422-
if err != nil {
423-
return errors.Wrap(err, "write state file")
424-
}
425-
426-
return nil
427-
}
428-
429-
func (m *MManager) serializeAndWriteStateSecret(state State) error {
430-
serialized, err := json.MarshalIndent(state, "", " ")
324+
s, err := m.getStateSerializer()
431325
if err != nil {
432-
return errors.Wrap(err, "serialize state")
326+
return errors.Wrap(err, "create state serializer")
433327
}
434328

435-
config, err := rest.InClusterConfig()
436-
if err != nil {
437-
return errors.Wrap(err, "get in cluster config")
438-
}
439-
440-
clientset, err := kubernetes.NewForConfig(config)
441-
if err != nil {
442-
return errors.Wrap(err, "get kubernetes client")
443-
}
444-
445-
secret, err := clientset.CoreV1().Secrets(m.V.GetString("secret-namespace")).Get(m.V.GetString("secret-name"), metav1.GetOptions{})
446-
if err != nil {
447-
return errors.Wrap(err, "get secret")
448-
}
449-
450-
secret.Data[m.V.GetString("secret-key")] = serialized
451-
debug := level.Debug(log.With(m.Logger, "method", "serializeHelmValues"))
452-
453-
debug.Log("event", "serializeAndWriteStateSecret", "name", secret.Name, "key", m.V.GetString("secret-key"))
454-
455-
_, err = clientset.CoreV1().Secrets(m.V.GetString("secret-namespace")).Update(secret)
456-
if err != nil {
457-
return errors.Wrap(err, "update secret")
329+
if err := s.Save(state); err != nil {
330+
return errors.Wrap(err, "save state")
458331
}
459332

460333
return nil

pkg/state/state_file.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package state
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/go-kit/kit/log"
9+
"github.com/go-kit/kit/log/level"
10+
"github.com/pkg/errors"
11+
"github.com/replicatedhq/ship/pkg/constants"
12+
"github.com/spf13/afero"
13+
)
14+
15+
type fileSerializer struct {
16+
fs afero.Afero
17+
logger log.Logger
18+
}
19+
20+
func newFileSerializer(fs afero.Afero, logger log.Logger) stateSerializer {
21+
return &fileSerializer{fs: fs, logger: logger}
22+
}
23+
24+
func (s *fileSerializer) Load() (State, error) {
25+
if _, err := s.fs.Stat(constants.StatePath); os.IsNotExist(err) {
26+
level.Debug(s.logger).Log("msg", "no saved state exists", "path", constants.StatePath)
27+
return State{}, nil
28+
}
29+
30+
serialized, err := s.fs.ReadFile(constants.StatePath)
31+
if err != nil {
32+
return State{}, errors.Wrap(err, "read state file")
33+
}
34+
35+
var state State
36+
if err := json.Unmarshal(serialized, &state); err != nil {
37+
return State{}, errors.Wrap(err, "unmarshal state")
38+
}
39+
40+
level.Debug(s.logger).Log("event", "state.resolve", "type", "versioned")
41+
return state, nil
42+
}
43+
44+
func (s *fileSerializer) Save(state State) error {
45+
serialized, err := json.MarshalIndent(state, "", " ")
46+
if err != nil {
47+
return errors.Wrap(err, "serialize state")
48+
}
49+
50+
err = s.fs.MkdirAll(filepath.Dir(constants.StatePath), 0700)
51+
if err != nil {
52+
return errors.Wrap(err, "mkdir state")
53+
}
54+
55+
err = s.fs.WriteFile(constants.StatePath, serialized, 0644)
56+
if err != nil {
57+
return errors.Wrap(err, "write state file")
58+
}
59+
60+
return nil
61+
}

0 commit comments

Comments
 (0)