github.com/replicatedcom/ship@v0.50.0/pkg/specs/replicatedapp/resolver.go (about) 1 package replicatedapp 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/json" 7 "fmt" 8 "net/url" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/go-kit/kit/log" 14 "github.com/go-kit/kit/log/level" 15 "github.com/mitchellh/cli" 16 "github.com/pkg/errors" 17 "github.com/replicatedhq/ship/pkg/api" 18 "github.com/replicatedhq/ship/pkg/constants" 19 "github.com/replicatedhq/ship/pkg/helpers/flags" 20 "github.com/replicatedhq/ship/pkg/specs/apptype" 21 "github.com/replicatedhq/ship/pkg/state" 22 "github.com/spf13/afero" 23 "github.com/spf13/viper" 24 yaml "gopkg.in/yaml.v3" 25 ) 26 27 type shaSummer func(release state.ShipRelease) string 28 type dater func() string 29 type resolver struct { 30 Logger log.Logger 31 Client *GraphQLClient 32 FS afero.Afero 33 StateManager state.Manager 34 UI cli.Ui 35 ShaSummer shaSummer 36 Dater dater 37 Runbook string 38 SetChannelName string 39 RunbookReleaseSemver string 40 SetChannelIcon string 41 SetGitHubContents []string 42 SetEntitlementsJSON string 43 IsEdit bool 44 } 45 46 // NewAppResolver builds a resolver from a Viper instance 47 func NewAppResolver( 48 v *viper.Viper, 49 logger log.Logger, 50 fs afero.Afero, 51 graphql *GraphQLClient, 52 stateManager state.Manager, 53 ui cli.Ui, 54 ) Resolver { 55 return &resolver{ 56 Logger: logger, 57 Client: graphql, 58 FS: fs, 59 UI: ui, 60 Runbook: flags.GetCurrentOrDeprecatedString(v, "runbook", "studio-file"), 61 SetChannelName: flags.GetCurrentOrDeprecatedString(v, "set-channel-name", "studio-channel-name"), 62 SetChannelIcon: flags.GetCurrentOrDeprecatedString(v, "set-channel-icon", "studio-channel-icon"), 63 SetGitHubContents: v.GetStringSlice("set-github-contents"), 64 SetEntitlementsJSON: v.GetString("set-entitlements-json"), 65 RunbookReleaseSemver: v.GetString("release-semver"), 66 IsEdit: v.GetBool("isEdit"), 67 StateManager: stateManager, 68 ShaSummer: func(release state.ShipRelease) string { 69 release.Entitlements.Signature = "" // entitlements signature is not stable 70 releaseJSON, err := json.Marshal(release) 71 if err != nil { 72 panic(errors.Wrap(err, "marshal release to json for content sha")) 73 } 74 75 return fmt.Sprintf("%x", sha256.Sum256(releaseJSON)) 76 }, 77 Dater: func() string { 78 // format consistent with what we get from GQL 79 return time.Now().UTC().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)") 80 }, 81 } 82 } 83 84 type Resolver interface { 85 ResolveAppRelease( 86 ctx context.Context, 87 selector *Selector, 88 app apptype.LocalAppCopy, 89 ) (*api.Release, error) 90 FetchRelease( 91 ctx context.Context, 92 selector *Selector, 93 ) (*state.ShipRelease, error) 94 RegisterInstall( 95 ctx context.Context, 96 selector Selector, 97 release *api.Release, 98 ) error 99 SetRunbook( 100 runbook string, 101 ) 102 ResolveEditRelease( 103 ctx context.Context, 104 ) (*api.Release, error) 105 } 106 107 // ResolveAppRelease uses the passed config options to get specs from pg.replicated.com or 108 // from a local runbook if so configured 109 func (r *resolver) ResolveAppRelease(ctx context.Context, selector *Selector, app apptype.LocalAppCopy) (*api.Release, error) { 110 debug := level.Debug(log.With(r.Logger, "method", "ResolveAppRelease")) 111 112 release, err := r.FetchRelease(ctx, selector) 113 if err != nil { 114 return nil, errors.Wrap(err, "fetch release") 115 } 116 117 license, err := r.FetchLicense(ctx, selector) 118 if err != nil { 119 return nil, errors.Wrap(err, "fetch license") 120 } 121 122 releaseName := release.ToReleaseMeta().ReleaseName() 123 debug.Log("event", "resolve.releaseName") 124 125 if err := r.StateManager.SerializeReleaseName(releaseName); err != nil { 126 debug.Log("event", "serialize.releaseName.fail", "err", err) 127 return nil, errors.Wrapf(err, "serialize helm release name") 128 } 129 130 result, err := r.persistRelease(release, license, selector) 131 if err != nil { 132 return nil, errors.Wrap(err, "persist and deserialize release") 133 } 134 135 result.Metadata.Type = app.GetType() 136 137 return result, nil 138 } 139 140 func (r *resolver) ResolveEditRelease(ctx context.Context) (*api.Release, error) { 141 stateData, err := r.StateManager.CachedState() 142 if err != nil { 143 return nil, errors.Wrap(err, "load state to resolve release") 144 } 145 146 result := &api.Release{ 147 Metadata: *stateData.ReleaseMetadata(), 148 } 149 150 if r.Runbook == "" { 151 result.Metadata.Type = "replicated.app" 152 } else { 153 result.Metadata.Type = "runbook.replicated.app" 154 } 155 156 if err = yaml.Unmarshal([]byte(stateData.UpstreamContents().AppRelease.Spec), &result.Spec); err != nil { 157 return nil, errors.Wrapf(err, "decode spec from persisted release") 158 } 159 160 if err = r.persistSpec([]byte(stateData.UpstreamContents().AppRelease.Spec)); err != nil { 161 return nil, errors.Wrapf(err, "write persisted spec to disk") 162 } 163 164 return result, nil 165 } 166 167 func (r *resolver) FetchLicense(ctx context.Context, selector *Selector) (*license, error) { 168 debug := level.Debug(log.With(r.Logger, "method", "FetchLicense")) 169 if r.Runbook != "" { 170 debug.Log("event", "license.fetch", "msg", "can't resolve license with runbooks") 171 return &license{}, nil 172 } 173 174 if selector.LicenseID == "" { 175 // TODO: support with customer ID 176 debug.Log("event", "license.fetch", "msg", "can't resolve license without license ID") 177 return &license{}, nil 178 } 179 180 license, err := r.Client.GetLicense(selector) 181 if err != nil { 182 return nil, errors.Wrapf(err, "get license") 183 } 184 185 return license, nil 186 } 187 188 // FetchRelease gets the release without persisting anything 189 func (r *resolver) FetchRelease(ctx context.Context, selector *Selector) (*state.ShipRelease, error) { 190 var err error 191 var release *state.ShipRelease 192 193 debug := level.Debug(log.With(r.Logger, "method", "FetchRelease")) 194 if r.Runbook != "" { 195 release, err = r.resolveRunbookRelease(selector) 196 if err != nil { 197 return nil, errors.Wrapf(err, "resolve runbook from %s", r.Runbook) 198 } 199 } else { 200 release, err = r.resolveCloudRelease(selector) 201 debug.Log("event", "spec.resolve", "err", err) 202 if err != nil { 203 return nil, errors.Wrapf(err, "resolve gql spec for %s", selector) 204 } 205 } 206 debug.Log("event", "spec.resolve.success", "err", err) 207 return release, nil 208 } 209 210 func (r *resolver) persistRelease(release *state.ShipRelease, license *license, selector *Selector) (*api.Release, error) { 211 debug := level.Debug(log.With(r.Logger, "method", "persistRelease")) 212 213 result := &api.Release{ 214 Metadata: release.ToReleaseMeta(), 215 } 216 result.Metadata.CustomerID = selector.CustomerID 217 result.Metadata.InstallationID = selector.InstallationID 218 result.Metadata.LicenseID = selector.LicenseID 219 result.Metadata.AppSlug = selector.AppSlug 220 result.Metadata.License = license.ToLicenseMeta() 221 result.Metadata.Installed = r.Dater() 222 223 if err := r.StateManager.SerializeAppMetadata(result.Metadata); err != nil { 224 return nil, errors.Wrap(err, "serialize app metadata") 225 } 226 227 contentSHA := r.ShaSummer(*release) 228 if err := r.StateManager.SerializeContentSHA(contentSHA); err != nil { 229 return nil, errors.Wrap(err, "serialize content sha") 230 } 231 232 if err := yaml.Unmarshal([]byte(release.Spec), &result.Spec); err != nil { 233 return nil, errors.Wrapf(err, "decode spec") 234 } 235 debug.Log("phase", "load-specs", "status", "complete", 236 "resolved-spec", fmt.Sprintf("%+v", result.Spec), 237 ) 238 239 if r.Runbook == "" { 240 releaseCopy := *release 241 242 upstreamContents := state.UpstreamContents{ 243 AppRelease: &releaseCopy, 244 } 245 err := r.StateManager.SerializeUpstreamContents(&upstreamContents) 246 if err != nil { 247 return nil, errors.Wrap(err, "persist upstream contents") 248 } 249 } 250 251 return result, nil 252 } 253 254 func (r *resolver) resolveCloudRelease(selector *Selector) (*state.ShipRelease, error) { 255 debug := level.Debug(log.With(r.Logger, "method", "resolveCloudSpec")) 256 257 var release *state.ShipRelease 258 var err error 259 client := r.Client 260 if selector.CustomerID != "" { 261 debug.Log("phase", "load-specs", "from", "gql", "addr", client.GQLServer.String(), "method", "customerID") 262 release, err = client.GetRelease(selector) 263 if err != nil { 264 return nil, err 265 } 266 } else { 267 debug.Log("phase", "load-specs", "from", "gql", "addr", client.GQLServer.String(), "method", "appSlug") 268 if selector.AppSlug == "" { 269 return nil, errors.New("either a customer ID or app slug must be provided") 270 } 271 release, err = client.GetSlugRelease(selector) 272 if err != nil { 273 if selector.LicenseID == "" { 274 debug.Log("event", "spec-resolve", "from", selector, "error", err) 275 276 var input string 277 input, err = r.UI.Ask("Please enter your license to continue: ") 278 if err != nil { 279 return nil, errors.Wrapf(err, "enter license from CLI") 280 } 281 282 selector.LicenseID = input 283 284 err = r.updateUpstream(*selector) 285 if err != nil { 286 return nil, errors.Wrapf(err, "persist updated upstream") 287 } 288 289 release, err = client.GetSlugRelease(selector) 290 } 291 292 if err != nil { 293 return nil, err 294 } 295 } 296 } 297 298 if err := r.persistSpec([]byte(release.Spec)); err != nil { 299 return nil, errors.Wrapf(err, "serialize last-used YAML to disk") 300 } 301 debug.Log("phase", "write-yaml", "from", release.Spec, "write-location", constants.ReleasePath) 302 303 return release, err 304 } 305 306 // persistSpec persists last-used YAML to disk at .ship/release.yml 307 func (r *resolver) persistSpec(specYAML []byte) error { 308 if err := r.FS.MkdirAll(filepath.Dir(constants.ReleasePath), 0700); err != nil { 309 return errors.Wrap(err, "mkdir yaml") 310 } 311 312 if err := r.FS.WriteFile(constants.ReleasePath, specYAML, 0644); err != nil { 313 return errors.Wrap(err, "write yaml file") 314 } 315 return nil 316 } 317 318 func (r *resolver) RegisterInstall(ctx context.Context, selector Selector, release *api.Release) error { 319 if r.Runbook != "" { 320 return nil 321 } 322 323 debug := level.Debug(log.With(r.Logger, "method", "RegisterRelease")) 324 325 debug.Log("phase", "register", "with", "gql", "addr", r.Client.GQLServer.String()) 326 327 err := r.Client.RegisterInstall(selector.GetBasicAuthUsername(), "", release.Metadata.ChannelID, release.Metadata.ReleaseID) 328 if err != nil { 329 return err 330 } 331 332 debug.Log("phase", "register", "status", "complete") 333 334 return nil 335 } 336 337 func (r *resolver) SetRunbook(runbook string) { 338 r.Runbook = runbook 339 } 340 341 func (r *resolver) loadFakeEntitlements() (*api.Entitlements, error) { 342 var entitlements api.Entitlements 343 if r.SetEntitlementsJSON == "" { 344 return &entitlements, nil 345 } 346 err := json.Unmarshal([]byte(r.SetEntitlementsJSON), &entitlements) 347 if err != nil { 348 return nil, errors.Wrap(err, "load entitlements json") 349 } 350 return &entitlements, nil 351 } 352 353 // read the upstream, get the host/path, and replace the query params with the ones from the provided selector 354 func (r *resolver) updateUpstream(selector Selector) error { 355 currentState, err := r.StateManager.CachedState() 356 if err != nil { 357 return errors.Wrap(err, "retrieve state") 358 } 359 currentUpstream := currentState.Upstream() 360 361 parsedUpstream, err := url.Parse(currentUpstream) 362 if err != nil { 363 return errors.Wrap(err, "parse upstream") 364 } 365 366 if !strings.HasSuffix(parsedUpstream.Path, "/") { 367 parsedUpstream.Path += "/" 368 } 369 370 return r.StateManager.SerializeUpstream(parsedUpstream.Path + "?" + selector.String()) 371 }