From d99ac8536904e57eada28ffb358e9f342a9978d1 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Fri, 14 Jul 2023 14:30:35 -0400 Subject: [PATCH] implemented naive migration from environment v0.3 to v0.4 (#369) --- cmd/zrok/status.go | 4 + cmd/zrok/update.go | 52 ++++++ environment/api.go | 67 +++---- environment/env_core/model.go | 27 +++ environment/env_v0_4/api.go | 168 ++++++++++++++++++ environment/env_v0_4/dirs.go | 55 ++++++ environment/env_v0_4/root.go | 321 ++++++++++++++++++++++++++++++++++ 7 files changed, 647 insertions(+), 47 deletions(-) create mode 100644 cmd/zrok/update.go create mode 100644 environment/env_v0_4/api.go create mode 100644 environment/env_v0_4/dirs.go create mode 100644 environment/env_v0_4/root.go diff --git a/cmd/zrok/status.go b/cmd/zrok/status.go index 4de14e54..fe6d00be 100644 --- a/cmd/zrok/status.go +++ b/cmd/zrok/status.go @@ -39,6 +39,10 @@ func (cmd *statusCommand) run(_ *cobra.Command, _ []string) { tui.Error("error loading environment", err) } + if !environment.IsLatest(env) { + tui.Warning(fmt.Sprintf("Your environment is out of date ('%v'), use '%v' to update (make a backup before updating!)\n", env.Metadata().V, tui.Code.Render("zrok update"))) + } + _, _ = fmt.Fprintf(os.Stdout, tui.Code.Render("Config")+":\n\n") t := table.NewWriter() t.SetOutputMirror(os.Stdout) diff --git a/cmd/zrok/update.go b/cmd/zrok/update.go new file mode 100644 index 00000000..da4df341 --- /dev/null +++ b/cmd/zrok/update.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "github.com/openziti/zrok/environment" + "github.com/openziti/zrok/tui" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(newUpdateCommand().cmd) +} + +type updateCommand struct { + cmd *cobra.Command +} + +func newUpdateCommand() *updateCommand { + cmd := &cobra.Command{ + Use: "update", + Short: "Update your environment to the latest version", + Args: cobra.NoArgs, + } + command := &updateCommand{cmd: cmd} + cmd.Run = command.run + return command +} + +func (cmd *updateCommand) run(_ *cobra.Command, _ []string) { + r, err := environment.LoadRoot() + if err != nil { + if !panicInstead { + tui.Error("unable to load environment", err) + } + panic(err) + } + + if environment.IsLatest(r) { + fmt.Printf("zrok environment is already the latest version at '%v'\n", r.Metadata().V) + return + } + + newR, err := environment.UpdateRoot(r) + if err != nil { + if !panicInstead { + tui.Error("unable to update environment", err) + } + panic(err) + } + + fmt.Printf("environment updated to '%v'\n", newR.Metadata().V) +} diff --git a/environment/api.go b/environment/api.go index 1c100635..213d9ed4 100644 --- a/environment/api.go +++ b/environment/api.go @@ -3,64 +3,37 @@ package environment import ( "github.com/openziti/zrok/environment/env_core" "github.com/openziti/zrok/environment/env_v0_3" - "github.com/openziti/zrok/rest_client_zrok" + "github.com/openziti/zrok/environment/env_v0_4" "github.com/pkg/errors" ) -// Root is the primary interface encapsulating the on-disk environment data. -type Root interface { - Metadata() *env_core.Metadata - Obliterate() error - - HasConfig() (bool, error) - Config() *env_core.Config - SetConfig(cfg *env_core.Config) error - - Client() (*rest_client_zrok.Zrok, error) - ApiEndpoint() (string, string) - - IsEnabled() bool - Environment() *env_core.Environment - SetEnvironment(env *env_core.Environment) error - DeleteEnvironment() error - - AccessIdentityName() string - ShareIdentityName() string - - ZitiIdentityNamed(name string) (string, error) - SaveZitiIdentityNamed(name, data string) error - DeleteZitiIdentityNamed(name string) error -} - -func LoadRoot() (Root, error) { - if assert, err := env_v0_3.Assert(); assert && err == nil { +func LoadRoot() (env_core.Root, error) { + if assert, err := env_v0_4.Assert(); assert && err == nil { + return env_v0_4.Load() + } else if assert, err := env_v0_3.Assert(); assert && err == nil { return env_v0_3.Load() } else { return nil, err } } -func ListRoots() ([]*env_core.Metadata, error) { - return nil, nil -} - -func LoadRootVersion(m *env_core.Metadata) (Root, error) { - if m == nil { - return nil, errors.Errorf("specify metadata version") +func IsLatest(r env_core.Root) bool { + if r == nil { + return false } - switch m.V { - case env_v0_3.V: - return env_v0_3.Load() - - default: - return nil, errors.Errorf("unknown metadata version '%v'", m.V) + if r.Metadata() == nil { + return false } + if r.Metadata().V == env_v0_4.V { + return true + } + return false } -func NeedsUpdate(r Root) bool { - return r.Metadata().V != env_v0_3.V -} - -func UpdateRoot(r Root) (Root, error) { - return nil, nil +func UpdateRoot(r env_core.Root) (env_core.Root, error) { + newR, err := env_v0_4.Update(r) + if err != nil { + return nil, errors.Wrap(err, "unable to update environment") + } + return newR, nil } diff --git a/environment/env_core/model.go b/environment/env_core/model.go index 2f7f9da7..7e5909dc 100644 --- a/environment/env_core/model.go +++ b/environment/env_core/model.go @@ -1,5 +1,32 @@ package env_core +import "github.com/openziti/zrok/rest_client_zrok" + +// Root is the primary interface encapsulating the on-disk environment data. +type Root interface { + Metadata() *Metadata + Obliterate() error + + HasConfig() (bool, error) + Config() *Config + SetConfig(cfg *Config) error + + Client() (*rest_client_zrok.Zrok, error) + ApiEndpoint() (string, string) + + IsEnabled() bool + Environment() *Environment + SetEnvironment(env *Environment) error + DeleteEnvironment() error + + AccessIdentityName() string + ShareIdentityName() string + + ZitiIdentityNamed(name string) (string, error) + SaveZitiIdentityNamed(name, data string) error + DeleteZitiIdentityNamed(name string) error +} + type Environment struct { Token string ZitiIdentity string diff --git a/environment/env_v0_4/api.go b/environment/env_v0_4/api.go new file mode 100644 index 00000000..fae20e8d --- /dev/null +++ b/environment/env_v0_4/api.go @@ -0,0 +1,168 @@ +package env_v0_4 + +import ( + "github.com/go-openapi/runtime" + httptransport "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/openziti/zrok/build" + "github.com/openziti/zrok/environment/env_core" + "github.com/openziti/zrok/rest_client_zrok" + "github.com/pkg/errors" + "net/url" + "os" + "path/filepath" + "regexp" +) + +func (r *Root) Metadata() *env_core.Metadata { + return r.meta +} + +func (r *Root) HasConfig() (bool, error) { + return r.cfg != nil, nil +} + +func (r *Root) Config() *env_core.Config { + return r.cfg +} + +func (r *Root) SetConfig(cfg *env_core.Config) error { + if err := assertMetadata(); err != nil { + return err + } + if err := saveConfig(cfg); err != nil { + return err + } + r.cfg = cfg + return nil +} + +func (r *Root) Client() (*rest_client_zrok.Zrok, error) { + apiEndpoint, _ := r.ApiEndpoint() + apiUrl, err := url.Parse(apiEndpoint) + if err != nil { + return nil, errors.Wrapf(err, "error parsing api endpoint '%v'", r) + } + transport := httptransport.New(apiUrl.Host, "/api/v1", []string{apiUrl.Scheme}) + transport.Producers["application/zrok.v1+json"] = runtime.JSONProducer() + transport.Consumers["application/zrok.v1+json"] = runtime.JSONConsumer() + + zrok := rest_client_zrok.New(transport, strfmt.Default) + v, err := zrok.Metadata.Version(nil) + if err != nil { + return nil, errors.Wrapf(err, "error getting version from api endpoint '%v': %v", apiEndpoint, err) + } + // allow reported version string to be optionally prefixed with + // "refs/heads/" or "refs/tags/" + re := regexp.MustCompile(`^(refs/(heads|tags)/)?` + build.Series) + if !re.MatchString(string(v.Payload)) { + return nil, errors.Errorf("expected a '%v' version, received: '%v'", build.Series, v.Payload) + } + + return zrok, nil +} + +func (r *Root) ApiEndpoint() (string, string) { + apiEndpoint := "https://api.zrok.io" + from := "binary" + + if r.Config() != nil && r.Config().ApiEndpoint != "" { + apiEndpoint = r.Config().ApiEndpoint + from = "config" + } + + env := os.Getenv("ZROK_API_ENDPOINT") + if env != "" { + apiEndpoint = env + from = "ZROK_API_ENDPOINT" + } + + if r.IsEnabled() { + apiEndpoint = r.Environment().ApiEndpoint + from = "env" + } + + return apiEndpoint, from +} + +func (r *Root) Environment() *env_core.Environment { + return r.env +} + +func (r *Root) SetEnvironment(env *env_core.Environment) error { + if err := assertMetadata(); err != nil { + return err + } + if err := saveEnvironment(env); err != nil { + return err + } + r.env = env + return nil +} + +func (r *Root) DeleteEnvironment() error { + ef, err := environmentFile() + if err != nil { + return errors.Wrap(err, "error getting environment file") + } + if err := os.Remove(ef); err != nil { + return errors.Wrap(err, "error removing environment file") + } + r.env = nil + return nil +} + +func (r *Root) IsEnabled() bool { + return r.env != nil +} + +func (r *Root) AccessIdentityName() string { + return "access" +} + +func (r *Root) ShareIdentityName() string { + return "share" +} + +func (r *Root) ZitiIdentityNamed(name string) (string, error) { + return identityFile(name) +} + +func (r *Root) SaveZitiIdentityNamed(name, data string) error { + if err := assertMetadata(); err != nil { + return err + } + zif, err := r.ZitiIdentityNamed(name) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(zif), os.FileMode(0700)); err != nil { + return errors.Wrapf(err, "error creating environment path '%v'", filepath.Dir(zif)) + } + if err := os.WriteFile(zif, []byte(data), os.FileMode(0600)); err != nil { + return errors.Wrapf(err, "error writing ziti identity file '%v'", zif) + } + return nil +} + +func (r *Root) DeleteZitiIdentityNamed(name string) error { + zif, err := r.ZitiIdentityNamed(name) + if err != nil { + return errors.Wrapf(err, "error getting ziti identity file path for '%v'", name) + } + if err := os.Remove(zif); err != nil { + return errors.Wrapf(err, "error removing ziti identity file '%v'", zif) + } + return nil +} + +func (r *Root) Obliterate() error { + zrd, err := rootDir() + if err != nil { + return err + } + if err := os.RemoveAll(zrd); err != nil { + return err + } + return nil +} diff --git a/environment/env_v0_4/dirs.go b/environment/env_v0_4/dirs.go new file mode 100644 index 00000000..b259fe09 --- /dev/null +++ b/environment/env_v0_4/dirs.go @@ -0,0 +1,55 @@ +package env_v0_4 + +import ( + "fmt" + "os" + "path/filepath" +) + +func rootDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".zrok"), nil +} + +func metadataFile() (string, error) { + zrd, err := rootDir() + if err != nil { + return "", err + } + return filepath.Join(zrd, "metadata.json"), nil +} + +func configFile() (string, error) { + zrd, err := rootDir() + if err != nil { + return "", err + } + return filepath.Join(zrd, "config.json"), nil +} + +func environmentFile() (string, error) { + zrd, err := rootDir() + if err != nil { + return "", err + } + return filepath.Join(zrd, "environment.json"), nil +} + +func identitiesDir() (string, error) { + zrd, err := rootDir() + if err != nil { + return "", err + } + return filepath.Join(zrd, "identities"), nil +} + +func identityFile(name string) (string, error) { + idd, err := identitiesDir() + if err != nil { + return "", err + } + return filepath.Join(idd, fmt.Sprintf("%v.json", name)), nil +} diff --git a/environment/env_v0_4/root.go b/environment/env_v0_4/root.go new file mode 100644 index 00000000..5a6f3191 --- /dev/null +++ b/environment/env_v0_4/root.go @@ -0,0 +1,321 @@ +package env_v0_4 + +import ( + "encoding/json" + "fmt" + "github.com/openziti/zrok/environment/env_core" + "github.com/openziti/zrok/environment/env_v0_3" + "github.com/pkg/errors" + "os" + "path/filepath" +) + +const V = "v0.4" + +type Root struct { + meta *env_core.Metadata + cfg *env_core.Config + env *env_core.Environment +} + +func Assert() (bool, error) { + exists, err := rootExists() + if err != nil { + return true, err + } + if exists { + meta, err := loadMetadata() + if err != nil { + return true, err + } + return meta.V == V, nil + } + return false, nil +} + +func Load() (*Root, error) { + r := &Root{} + exists, err := rootExists() + if err != nil { + return nil, err + } + if exists { + if meta, err := loadMetadata(); err == nil { + r.meta = meta + } else { + return nil, err + } + + if cfg, err := loadConfig(); err == nil { + r.cfg = cfg + } + + if env, err := loadEnvironment(); err == nil { + r.env = env + } + + } else { + root, err := rootDir() + if err != nil { + return nil, err + } + r.meta = &env_core.Metadata{ + V: V, + RootPath: root, + } + } + return r, nil +} + +func Update(r env_core.Root) (env_core.Root, error) { + if r == nil || r.Metadata() == nil { + return nil, errors.Errorf("nil root") + } + if r.Metadata().V != env_v0_3.V { + return nil, errors.Errorf("expecting version '%v'", env_v0_3.V) + } + + newR := &Root{meta: r.Metadata(), cfg: r.Config(), env: r.Environment()} + + oldAccessF, err := r.ZitiIdentityNamed(r.AccessIdentityName()) + if err != nil { + return nil, err + } + _, err = os.Stat(oldAccessF) + if err == nil { + newAccessF, err := newR.ZitiIdentityNamed(newR.AccessIdentityName()) + if err != nil { + return nil, err + } + if err := os.Rename(oldAccessF, newAccessF); err != nil { + return nil, err + } + fmt.Printf("renamed '%v' -> '%v'\n", oldAccessF, newAccessF) + } else if !os.IsNotExist(err) { + return nil, err + } + + oldShareF, err := r.ZitiIdentityNamed(r.ShareIdentityName()) + if err != nil { + return nil, err + } + _, err = os.Stat(oldShareF) + if err == nil { + newShareF, err := newR.ZitiIdentityNamed(newR.ShareIdentityName()) + if err != nil { + return nil, err + } + if err := os.Rename(oldShareF, newShareF); err != nil { + return nil, err + } + fmt.Printf("renamed '%v' -> '%v'\n", oldShareF, newShareF) + } + + if err := writeMetadata(); err != nil { + return nil, err + } + + meta, err := loadMetadata() + if err != nil { + return nil, err + } + newR.meta = meta + + return newR, nil +} + +func rootExists() (bool, error) { + mf, err := metadataFile() + if err != nil { + return false, err + } + _, err = os.Stat(mf) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +func assertMetadata() error { + exists, err := rootExists() + if err != nil { + return err + } + if !exists { + if err := writeMetadata(); err != nil { + return err + } + } + return nil +} + +func loadMetadata() (*env_core.Metadata, error) { + mf, err := metadataFile() + if err != nil { + return nil, err + } + data, err := os.ReadFile(mf) + if err != nil { + return nil, err + } + m := &metadata{} + if err := json.Unmarshal(data, m); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling metadata file '%v'", mf) + } + if m.V != V { + return nil, errors.Errorf("got metadata version '%v', expected '%v'", m.V, V) + } + rf, err := rootDir() + if err != nil { + return nil, err + } + out := &env_core.Metadata{ + V: m.V, + RootPath: rf, + } + return out, nil +} + +func writeMetadata() error { + mf, err := metadataFile() + if err != nil { + return err + } + data, err := json.Marshal(&metadata{V: V}) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(mf), os.FileMode(0700)); err != nil { + return err + } + if err := os.WriteFile(mf, data, os.FileMode(0600)); err != nil { + return err + } + return nil +} + +func loadConfig() (*env_core.Config, error) { + cf, err := configFile() + if err != nil { + return nil, errors.Wrap(err, "error getting config file path") + } + data, err := os.ReadFile(cf) + if err != nil { + return nil, errors.Wrapf(err, "error reading config file '%v'", cf) + } + cfg := &config{} + if err := json.Unmarshal(data, cfg); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling config file '%v'", cf) + } + out := &env_core.Config{ + ApiEndpoint: cfg.ApiEndpoint, + } + return out, nil +} + +func saveConfig(cfg *env_core.Config) error { + in := &config{ApiEndpoint: cfg.ApiEndpoint} + data, err := json.MarshalIndent(in, "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling config") + } + cf, err := configFile() + if err != nil { + return errors.Wrap(err, "error getting config file path") + } + if err := os.MkdirAll(filepath.Dir(cf), os.FileMode(0700)); err != nil { + return errors.Wrapf(err, "error creating environment path '%v'", filepath.Dir(cf)) + } + if err := os.WriteFile(cf, data, os.FileMode(0600)); err != nil { + return errors.Wrap(err, "error saving config file") + } + return nil +} + +func isEnabled() (bool, error) { + ef, err := environmentFile() + if err != nil { + return false, errors.Wrap(err, "error getting environment file path") + } + _, err = os.Stat(ef) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, errors.Wrapf(err, "error stat-ing environment file '%v'", ef) + } + return true, nil +} + +func loadEnvironment() (*env_core.Environment, error) { + ef, err := environmentFile() + if err != nil { + return nil, errors.Wrap(err, "error getting environment file") + } + data, err := os.ReadFile(ef) + if err != nil { + return nil, errors.Wrapf(err, "error reading environment file '%v'", ef) + } + env := &environment{} + if err := json.Unmarshal(data, env); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling environment file '%v'", ef) + } + out := &env_core.Environment{ + Token: env.Token, + ZitiIdentity: env.ZId, + ApiEndpoint: env.ApiEndpoint, + } + return out, nil +} + +func saveEnvironment(env *env_core.Environment) error { + in := &environment{ + Token: env.Token, + ZId: env.ZitiIdentity, + ApiEndpoint: env.ApiEndpoint, + } + data, err := json.MarshalIndent(in, "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling environment") + } + ef, err := environmentFile() + if err != nil { + return errors.Wrap(err, "error getting environment file") + } + if err := os.MkdirAll(filepath.Dir(ef), os.FileMode(0700)); err != nil { + return errors.Wrapf(err, "error creating environment path '%v'", filepath.Dir(ef)) + } + if err := os.WriteFile(ef, data, os.FileMode(0600)); err != nil { + return errors.Wrap(err, "error saving environment file") + } + return nil +} + +func deleteEnvironment() error { + ef, err := environmentFile() + if err != nil { + return errors.Wrap(err, "error getting environment file") + } + if err := os.Remove(ef); err != nil { + return errors.Wrap(err, "error removing environment file") + } + + return nil +} + +type metadata struct { + V string `json:"v"` +} + +type config struct { + ApiEndpoint string `json:"api_endpoint"` +} + +type environment struct { + Token string `json:"zrok_token"` + ZId string `json:"ziti_identity"` + ApiEndpoint string `json:"api_endpoint"` +}