github.com/replicatedcom/ship@v0.50.0/pkg/specs/interface.go (about) 1 package specs 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "os" 8 "path/filepath" 9 10 "github.com/go-kit/kit/log" 11 "github.com/go-kit/kit/log/level" 12 "github.com/pkg/errors" 13 "github.com/replicatedhq/ship/pkg/api" 14 "github.com/replicatedhq/ship/pkg/constants" 15 "github.com/replicatedhq/ship/pkg/specs/apptype" 16 "github.com/replicatedhq/ship/pkg/specs/replicatedapp" 17 "github.com/replicatedhq/ship/pkg/util" 18 yaml "gopkg.in/yaml.v3" 19 ) 20 21 func (r *Resolver) ResolveUnforkRelease(ctx context.Context, upstream string, forked string) (*api.Release, error) { 22 debug := log.With(level.Debug(r.Logger), "method", "ResolveUnforkReleases") 23 r.ui.Info(fmt.Sprintf("Reading %s and %s ...", upstream, forked)) 24 25 // Prepare the upstream 26 r.ui.Info("Determining upstream application type ...") 27 upstreamApp, err := r.appTypeInspector.DetermineApplicationType(ctx, upstream) 28 if err != nil { 29 return nil, errors.Wrapf(err, "determine type of %s", upstream) 30 } 31 debug.Log("event", "applicationType.resolve", "type", upstreamApp.GetType()) 32 r.ui.Info(fmt.Sprintf("Detected upstream application type %s", upstreamApp.GetType())) 33 34 debug.Log("event", "versionedUpstream.resolve", "type", upstreamApp.GetType()) 35 versionedUpstream, err := r.maybeCreateVersionedUpstream(upstream) 36 if err != nil { 37 return nil, errors.Wrap(err, "resolve versioned upstream") 38 } 39 40 debug.Log("event", "upstream.Serialize", "for", upstreamApp.GetLocalPath(), "upstream", versionedUpstream) 41 err = r.StateManager.SerializeUpstream(versionedUpstream) 42 if err != nil { 43 return nil, errors.Wrapf(err, "write upstream") 44 } 45 46 // Prepare the fork 47 r.ui.Info("Determining forked application type ...") 48 forkedApp, err := r.appTypeInspector.DetermineApplicationType(ctx, forked) 49 if err != nil { 50 return nil, errors.Wrapf(err, "determine type of %s", forked) 51 } 52 53 debug.Log("event", "applicationType.resolve", "type", forkedApp.GetType()) 54 r.ui.Info(fmt.Sprintf("Detected forked application type %s", forkedApp.GetType())) 55 56 if forkedApp.GetType() == "helm" && upstreamApp.GetType() == "k8s" { 57 return nil, errors.New("Unsupported fork and upstream combination") 58 } 59 60 var forkedAsset api.Asset 61 switch forkedApp.GetType() { 62 case "helm": 63 forkedAsset = api.Asset{ 64 Helm: &api.HelmAsset{ 65 AssetShared: api.AssetShared{ 66 Dest: constants.UnforkForkedBasePath, 67 }, 68 Local: &api.LocalHelmOpts{ 69 ChartRoot: constants.HelmChartForkedPath, 70 }, 71 ValuesFrom: &api.ValuesFrom{ 72 Path: constants.HelmChartForkedPath, 73 SaveToState: true, 74 }, 75 Upstream: forked, 76 }, 77 } 78 case "k8s": 79 forkedAsset = api.Asset{ 80 Local: &api.LocalAsset{ 81 AssetShared: api.AssetShared{ 82 Dest: constants.UnforkForkedBasePath, 83 }, 84 Path: constants.HelmChartForkedPath, 85 }, 86 } 87 default: 88 return nil, errors.Errorf("unknown forked application type %q", forkedApp.GetType()) 89 } 90 91 var upstreamAsset api.Asset 92 switch upstreamApp.GetType() { 93 case "helm": 94 upstreamAsset = api.Asset{ 95 Helm: &api.HelmAsset{ 96 AssetShared: api.AssetShared{ 97 Dest: constants.KustomizeBasePath, 98 }, 99 Local: &api.LocalHelmOpts{ 100 ChartRoot: constants.HelmChartPath, 101 }, 102 ValuesFrom: &api.ValuesFrom{ 103 Path: constants.HelmChartPath, 104 }, 105 Upstream: upstream, 106 }, 107 } 108 case "k8s": 109 upstreamAsset = api.Asset{ 110 Local: &api.LocalAsset{ 111 AssetShared: api.AssetShared{ 112 Dest: constants.KustomizeBasePath, 113 }, 114 Path: constants.HelmChartPath, 115 }, 116 } 117 default: 118 return nil, errors.Errorf("unknown upstream application type %q", upstreamApp.GetType()) 119 } 120 121 defaultRelease := r.DefaultHelmUnforkRelease(upstreamAsset, forkedAsset) 122 123 return r.resolveUnforkRelease( 124 ctx, 125 upstream, 126 forked, 127 upstreamApp, 128 forkedApp, 129 constants.HelmChartPath, 130 constants.HelmChartForkedPath, 131 &defaultRelease, 132 ) 133 } 134 135 // A resolver turns a target string into a release. 136 // 137 // A "target string" is something like 138 // 139 // github.com/helm/charts/stable/nginx-ingress 140 // replicated.app/cool-ci-tool?customer_id=...&installation_id=... 141 // file::/home/bob/apps/ship.yaml 142 // file::/home/luke/my-charts/proton-torpedoes 143 func (r *Resolver) ResolveRelease(ctx context.Context, upstream string) (*api.Release, error) { 144 debug := log.With(level.Debug(r.Logger), "method", "ResolveRelease") 145 r.ui.Info(fmt.Sprintf("Reading %s ...", upstream)) 146 147 r.ui.Info("Determining application type ...") 148 app, err := r.appTypeInspector.DetermineApplicationType(ctx, upstream) 149 if err != nil { 150 return nil, errors.Wrapf(err, "determine type of %s", upstream) 151 } 152 debug.Log("event", "applicationType.resolve", "type", app.GetType()) 153 r.ui.Info(fmt.Sprintf("Detected application type %s", app.GetType())) 154 155 debug.Log("event", "versionedUpstream.resolve", "type", app.GetType()) 156 versionedUpstream, err := r.maybeCreateVersionedUpstream(upstream) 157 if err != nil { 158 return nil, errors.Wrap(err, "resolve versioned upstream") 159 } 160 161 debug.Log("event", "upstream.Serialize", "for", app.GetLocalPath(), "upstream", versionedUpstream) 162 163 if !r.isEdit { 164 err = r.StateManager.SerializeUpstream(versionedUpstream) 165 if err != nil { 166 return nil, errors.Wrapf(err, "write upstream") 167 } 168 } 169 170 if app.GetType() != "replicated.app" { 171 debug.Log("event", "persist app state") 172 persistPath := app.GetLocalPath() 173 if app.GetType() == "runbook.replicated.app" { 174 persistPath = filepath.Dir(app.GetLocalPath()) 175 } 176 177 err = r.persistToState(persistPath) 178 if err != nil { 179 return nil, errors.Wrapf(err, "persist %s to state from path %s", app.GetType(), persistPath) 180 } 181 } 182 183 switch app.GetType() { 184 185 case "helm": 186 defaultRelease := r.DefaultHelmRelease(app.GetLocalPath(), upstream) 187 188 return r.resolveRelease( 189 ctx, 190 upstream, 191 app, 192 constants.HelmChartPath, 193 &defaultRelease, 194 true, 195 true, 196 ) 197 198 case "k8s": 199 defaultRelease := r.DefaultRawRelease(constants.KustomizeBasePath) 200 201 return r.resolveRelease( 202 ctx, 203 upstream, 204 app, 205 constants.KustomizeBasePath, 206 &defaultRelease, 207 false, 208 true, 209 ) 210 211 case "runbook.replicated.app": 212 r.AppResolver.SetRunbook(app.GetLocalPath()) 213 fallthrough 214 case "replicated.app": 215 if r.isEdit { 216 return r.AppResolver.ResolveEditRelease(ctx) 217 } 218 219 parsed, err := url.Parse(upstream) 220 if err != nil { 221 return nil, errors.Wrapf(err, "parse url %s", upstream) 222 } 223 selector := (&replicatedapp.Selector{}).UnmarshalFrom(parsed) 224 return r.AppResolver.ResolveAppRelease(ctx, selector, app) 225 226 case "inline.replicated.app": 227 return r.resolveInlineShipYAMLRelease( 228 ctx, 229 upstream, 230 app, 231 ) 232 233 } 234 235 return nil, errors.Errorf("unknown application type %q for upstream %q", app.GetType(), upstream) 236 } 237 238 func (r *Resolver) resolveUnforkRelease( 239 ctx context.Context, 240 upstream string, 241 forked string, 242 upstreamApp apptype.LocalAppCopy, 243 forkedApp apptype.LocalAppCopy, 244 destUpstreamPath string, 245 destForkedPath string, 246 defaultSpec *api.Spec, 247 ) (*api.Release, error) { 248 var releaseName string 249 debug := log.With(level.Debug(r.Logger), "method", "resolveUnforkReleases") 250 251 if r.Viper.GetBool("rm-asset-dest") { 252 err := r.FS.RemoveAll(destUpstreamPath) 253 if err != nil { 254 return nil, errors.Wrapf(err, "remove asset dest %s", destUpstreamPath) 255 } 256 257 err = r.FS.RemoveAll(destForkedPath) 258 if err != nil { 259 return nil, errors.Wrapf(err, "remove asset dest %s", destForkedPath) 260 } 261 } 262 263 err := util.BailIfPresent(r.FS, destUpstreamPath, debug) 264 if err != nil { 265 return nil, errors.Wrapf(err, "backup %s", destUpstreamPath) 266 } 267 268 err = r.FS.MkdirAll(filepath.Dir(destUpstreamPath), 0777) 269 if err != nil { 270 return nil, errors.Wrapf(err, "mkdir %s", destUpstreamPath) 271 } 272 273 err = r.FS.MkdirAll(filepath.Dir(destForkedPath), 0777) 274 if err != nil { 275 return nil, errors.Wrapf(err, "mkdir %s", destForkedPath) 276 } 277 278 err = r.FS.Rename(upstreamApp.GetLocalPath(), destUpstreamPath) 279 if err != nil { 280 return nil, errors.Wrapf(err, "move %s to %s", upstreamApp.GetLocalPath(), destUpstreamPath) 281 } 282 283 err = r.FS.Rename(forkedApp.GetLocalPath(), destForkedPath) 284 if err != nil { 285 return nil, errors.Wrapf(err, "move %s to %s", forkedApp.GetLocalPath(), destForkedPath) 286 } 287 288 if forkedApp.GetType() == "k8s" { 289 // Pre-emptively need to split here in order to get the release name before 290 // helm template is run on the upstream 291 if err := util.MaybeSplitMultidocYaml(ctx, r.FS, destForkedPath); err != nil { 292 return nil, errors.Wrapf(err, "maybe split multidoc in %s", destForkedPath) 293 } 294 295 debug.Log("event", "maybeGetReleaseName") 296 releaseName, err = r.maybeGetReleaseName(destForkedPath) 297 if err != nil { 298 return nil, errors.Wrap(err, "maybe get release name") 299 } 300 } 301 302 upstreamMetadata, err := r.resolveMetadata(context.Background(), upstream, destUpstreamPath, upstreamApp.GetType()) 303 if err != nil { 304 return nil, errors.Wrapf(err, "resolve metadata for %s", destUpstreamPath) 305 } 306 307 release := &api.Release{ 308 Metadata: api.ReleaseMetadata{ 309 ShipAppMetadata: *upstreamMetadata, 310 }, 311 Spec: *defaultSpec, 312 } 313 314 if releaseName == "" { 315 releaseName = release.Metadata.ReleaseName() 316 } 317 318 if err := r.StateManager.SerializeReleaseName(releaseName); err != nil { 319 debug.Log("event", "serialize.releaseName.fail", "err", err) 320 return nil, errors.Wrapf(err, "serialize helm release name") 321 } 322 323 return release, nil 324 } 325 326 func (r *Resolver) maybeGetReleaseName(path string) (string, error) { 327 type k8sReleaseMetadata struct { 328 Metadata struct { 329 Labels struct { 330 Release string `yaml:"release"` 331 } `yaml:"labels"` 332 } `yaml:"metadata"` 333 } 334 335 files, err := r.FS.ReadDir(path) 336 if err != nil { 337 return "", errors.Wrapf(err, "read dir %s", path) 338 } 339 340 for _, file := range files { 341 if filepath.Ext(file.Name()) == ".yaml" || filepath.Ext(file.Name()) == ".yml" { 342 fileB, err := r.FS.ReadFile(filepath.Join(path, file.Name())) 343 if err != nil { 344 return "", errors.Wrapf(err, "read file %s", path) 345 } 346 347 releaseMetadata := k8sReleaseMetadata{} 348 if err := yaml.Unmarshal(fileB, &releaseMetadata); err != nil { 349 return "", errors.Wrapf(err, "unmarshal for release metadata %s", path) 350 } 351 352 if releaseMetadata.Metadata.Labels.Release != "" { 353 return releaseMetadata.Metadata.Labels.Release, nil 354 } 355 } 356 } 357 358 return "", nil 359 } 360 361 func (r *Resolver) resolveRelease( 362 ctx context.Context, 363 upstream string, 364 app apptype.LocalAppCopy, 365 destPath string, 366 defaultSpec *api.Spec, 367 keepOriginal bool, 368 tryUseUpstreamShipYAML bool, 369 ) (*api.Release, error) { 370 debug := log.With(level.Debug(r.Logger), "method", "resolveRelease") 371 372 if r.Viper.GetBool("rm-asset-dest") { 373 err := r.FS.RemoveAll(destPath) 374 if err != nil { 375 return nil, errors.Wrapf(err, "remove asset dest %s", destPath) 376 } 377 } 378 379 err := util.BailIfPresent(r.FS, destPath, debug) 380 if err != nil { 381 return nil, errors.Wrapf(err, "backup %s", destPath) 382 } 383 384 if !keepOriginal { 385 err = r.FS.Rename(app.GetLocalPath(), destPath) 386 if err != nil { 387 return nil, errors.Wrapf(err, "move %s to %s", app.GetLocalPath(), destPath) 388 } 389 } else { 390 // instead of renaming, copy files from localPath to destPath 391 err = r.recursiveCopy(app.GetLocalPath(), destPath) 392 if err != nil { 393 return nil, errors.Wrapf(err, "copy %s to %s", app.GetLocalPath(), destPath) 394 } 395 } 396 397 metadata, err := r.resolveMetadata(context.Background(), upstream, destPath, app.GetType()) 398 if err != nil { 399 return nil, errors.Wrapf(err, "resolve metadata for %s", destPath) 400 } 401 402 var spec *api.Spec 403 if tryUseUpstreamShipYAML { 404 debug.Log("event", "check upstream for ship.yaml") 405 spec, err = r.maybeGetShipYAML(ctx, destPath) 406 if err != nil { 407 return nil, errors.Wrapf(err, "resolve ship.yaml release for %s", destPath) 408 } 409 } 410 411 if spec == nil { 412 debug.Log("event", "no ship.yaml for release") 413 r.ui.Info("ship.yaml not found in upstream, generating default lifecycle for application ...") 414 spec = defaultSpec 415 } 416 417 if metadata == nil { 418 metadata = &api.ShipAppMetadata{} 419 } 420 421 release := &api.Release{ 422 Metadata: api.ReleaseMetadata{ 423 ShipAppMetadata: *metadata, 424 Type: app.GetType(), 425 }, 426 Spec: *spec, 427 } 428 429 currentState, err := r.StateManager.CachedState() 430 if err != nil { 431 return nil, errors.Wrap(err, "try load") 432 } 433 434 releaseName := currentState.CurrentReleaseName() 435 if releaseName == "" { 436 debug.Log("event", "resolve.releaseName.fromRelease") 437 releaseName = release.Metadata.ReleaseName() 438 } 439 440 if err := r.StateManager.SerializeReleaseName(releaseName); err != nil { 441 debug.Log("event", "serialize.releaseName.fail", "err", err) 442 return nil, errors.Wrapf(err, "serialize helm release name") 443 } 444 445 return release, nil 446 } 447 448 func (r *Resolver) recursiveCopy(sourceDir, destDir string) error { 449 err := r.FS.MkdirAll(destDir, os.FileMode(0777)) 450 if err != nil { 451 return errors.Wrapf(err, "create dest dir %s", destDir) 452 } 453 srcFiles, err := r.FS.ReadDir(sourceDir) 454 if err != nil { 455 return errors.Wrapf(err, "") 456 } 457 for _, file := range srcFiles { 458 if file.IsDir() { 459 err = r.recursiveCopy(filepath.Join(sourceDir, file.Name()), filepath.Join(destDir, file.Name())) 460 if err != nil { 461 return errors.Wrapf(err, "copy dir %s", file.Name()) 462 } 463 } else { 464 // is file 465 contents, err := r.FS.ReadFile(filepath.Join(sourceDir, file.Name())) 466 if err != nil { 467 return errors.Wrapf(err, "read file %s to copy", file.Name()) 468 } 469 470 err = r.FS.WriteFile(filepath.Join(destDir, file.Name()), contents, file.Mode()) 471 if err != nil { 472 return errors.Wrapf(err, "write file %s to copy", file.Name()) 473 } 474 } 475 } 476 return nil 477 } 478 479 func (r *Resolver) resolveInlineShipYAMLRelease( 480 ctx context.Context, 481 upstream string, 482 app apptype.LocalAppCopy, 483 ) (*api.Release, error) { 484 debug := log.With(level.Debug(r.Logger), "method", "resolveInlineShipYAMLRelease") 485 metadata, err := r.resolveMetadata(context.Background(), upstream, app.GetLocalPath(), app.GetType()) 486 if err != nil { 487 return nil, errors.Wrapf(err, "resolve metadata for %s", app.GetLocalPath()) 488 } 489 debug.Log("event", "check upstream for ship.yaml") 490 spec, err := r.maybeGetShipYAML(ctx, app.GetLocalPath()) 491 if err != nil || spec == nil { 492 return nil, errors.Wrapf(err, "resolve ship.yaml release for %s", app.GetLocalPath()) 493 } 494 release := &api.Release{ 495 Metadata: api.ReleaseMetadata{ 496 ShipAppMetadata: *metadata, 497 Type: app.GetType(), 498 }, 499 Spec: *spec, 500 } 501 releaseName := release.Metadata.ReleaseName() 502 debug.Log("event", "resolve.releaseName") 503 if err := r.StateManager.SerializeReleaseName(releaseName); err != nil { 504 debug.Log("event", "serialize.releaseName.fail", "err", err) 505 return nil, errors.Wrapf(err, "serialize helm release name") 506 } 507 return release, nil 508 }