diff --git a/cmd/validate.go b/cmd/validate.go index 67822160..a2d5d246 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -5,10 +5,21 @@ import ( "github.com/urfave/cli/v2" ) +var ValidateUpgradeCommand = &cli.Command{ + Name: "upgrade", + Action: PipelineAction(pipelines.ValidatePackageUpgrade), + Usage: "Validates if a .deb or a .rpm package (--from) can be upgraded by another .deb or .rpm package (--to)", + Flags: JoinFlagsWithDefault( + PackageInputFlags, + GCPFlags, + ), +} + var ValidateCommand = &cli.Command{ Name: "validate", Action: PipelineAction(pipelines.ValidatePackage), - Description: "Validates a grafana.tar.gz for the given distributions (--distro) placed in the destination directory (--destination)", + Description: "Validates grafana .tar.gz, .deb, .rpm and .docker.tar.gz packages and places the results in the destination directory (--destination)", + Subcommands: []*cli.Command{ValidateUpgradeCommand}, Flags: JoinFlagsWithDefault( PackageInputFlags, GrafanaFlags, diff --git a/pipelines/package_installer.go b/pipelines/package_installer.go index 98f85b52..2545d674 100644 --- a/pipelines/package_installer.go +++ b/pipelines/package_installer.go @@ -13,6 +13,7 @@ import ( "dagger.io/dagger" "github.com/grafana/grafana-build/containers" "github.com/grafana/grafana-build/executil" + "github.com/grafana/grafana-build/versions" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" ) @@ -54,8 +55,18 @@ func PackageInstaller(ctx context.Context, d *dagger.Client, args PipelineArgs, fmt.Sprintf("--version=%s", tarOpts.Version), fmt.Sprintf("--package=%s", "/src/"+name), } + + vopts = versions.OptionsFor(tarOpts.Version) ) + // If this is a debian installer and this version had a prerm script (introduced in v9.5)... + // TODO: this logic means that rpms can't also have a beforeremove. Not important at the moment because it's static (in pipelines/rpm.go) and it doesn't have beforeremove set. + if vopts.DebPreRM.IsSet && vopts.DebPreRM.Value && opts.PackageType == "deb" { + if opts.BeforeRemove != "" { + fpmArgs = append(fpmArgs, fmt.Sprintf("--before-remove=%s", opts.BeforeRemove)) + } + } + for _, c := range opts.ConfigFiles { fpmArgs = append(fpmArgs, fmt.Sprintf("--config-files=%s", c[1])) } @@ -64,10 +75,6 @@ func PackageInstaller(ctx context.Context, d *dagger.Client, args PipelineArgs, fpmArgs = append(fpmArgs, fmt.Sprintf("--after-install=%s", opts.AfterInstall)) } - if opts.BeforeRemove != "" { - fpmArgs = append(fpmArgs, fmt.Sprintf("--before-remove=%s", opts.BeforeRemove)) - } - for _, d := range opts.Depends { fpmArgs = append(fpmArgs, fmt.Sprintf("--depends=%s", d)) } @@ -115,14 +122,7 @@ func PackageInstaller(ctx context.Context, d *dagger.Client, args PipelineArgs, WithEnvVariable("XZ_DEFAULTS", "-T0"). WithExec([]string{"tar", "--strip-components=1", "-xvf", "/src/grafana.tar.gz", "-C", "/src"}). WithExec([]string{"ls", "-al", "/src"}) - // Fix for issue where some older versions did not have BeforeRemove or AfterInstall scripts: Still add them, but provide empty scripts that do nothing if they don't exist - if opts.AfterInstall != "" { - container = container.WithExec([]string{"touch", opts.AfterInstall}) - } - // Fix for issue where some older versions did not have BeforeRemove or AfterInstall scripts: Still add them, but provide empty scripts that do nothing if they don't exist - if opts.BeforeRemove != "" { - container = container.WithExec([]string{"touch", opts.BeforeRemove}) - } + container = container. WithExec(append([]string{"mkdir", "-p"}, packagePaths...)). // the "wrappers" scripts are the same as grafana-cli/grafana-server but with some extra shell commands before/after execution. diff --git a/pipelines/package_names.go b/pipelines/package_names.go index 5738b182..5fa72348 100644 --- a/pipelines/package_names.go +++ b/pipelines/package_names.go @@ -14,6 +14,7 @@ type TarFileOpts struct { // Edition is the flavor text after "grafana-", like "enterprise". Edition string Distro executil.Distribution + Suffix string } func WithoutExt(name string) string { @@ -79,8 +80,10 @@ func TarOptsFromFileName(filename string) TarFileOpts { arch = strings.Join([]string{archv[0], archv[1]}, "/") } edition := "" + suffix := "" if n := strings.Split(name, "-"); len(n) != 1 { edition = n[1] + suffix = fmt.Sprintf("-%s", n[1]) } return TarFileOpts{ @@ -88,6 +91,7 @@ func TarOptsFromFileName(filename string) TarFileOpts { Version: version, BuildID: buildID, Distro: executil.Distribution(strings.Join([]string{os, arch}, "/")), + Suffix: suffix, } } diff --git a/pipelines/package_validate.go b/pipelines/package_validate.go index 8bea21ef..539d61be 100644 --- a/pipelines/package_validate.go +++ b/pipelines/package_validate.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "path/filepath" "strings" "dagger.io/dagger" @@ -54,6 +55,19 @@ func ValidatePackage(ctx context.Context, d *dagger.Client, src *dagger.Director return grp.Wait() } +func ValidatePackageUpgrade(ctx context.Context, d *dagger.Client, src *dagger.Directory, args PipelineArgs) error { + packages, err := containers.GetPackages(ctx, d, args.PackageInputOpts, args.GCPOpts) + if err != nil { + return err + } + + if len(packages) < 2 { + return fmt.Errorf("at least two packages required for upgrade") + } + + return validateUpgrade(ctx, d, packages, args.PackageInputOpts.Packages) +} + func distroPlatform(distro executil.Distribution) dagger.Platform { platform := executil.Platform(distro) if _, arch := executil.OSAndArch(distro); arch == "arm" { @@ -97,7 +111,7 @@ func validateDocker(ctx context.Context, d *dagger.Client, pkg *dagger.File, src platform = distroPlatform(taropts.Distro) ) - log.Printf("Validating docker image for v%s-%s using platform %s\n", taropts.Version, taropts.Edition, taropts.Distro) + log.Printf("Validating docker image for v%s%s using platform %s\n", taropts.Version, taropts.Suffix, taropts.Distro) // This grafana service runs in the background for the e2e tests // Just guessing that maybe we need to add the "PACKAGE" environment variable here to prevent weird caching collisions @@ -127,7 +141,7 @@ func validateDeb(ctx context.Context, d *dagger.Client, deb *dagger.File, src *d platform = distroPlatform(taropts.Distro) ) - log.Printf("Validating deb package for v%s-%s using debian:latest and platform %s\n", taropts.Version, taropts.Edition, taropts.Distro) + log.Printf("Validating deb package for v%s%s using debian:latest and platform %s\n", taropts.Version, taropts.Suffix, taropts.Distro) // This grafana service runs in the background for the e2e tests service := d.Container(dagger.ContainerOpts{ @@ -163,7 +177,7 @@ func validateRpm(ctx context.Context, d *dagger.Client, rpm *dagger.File, src *d platform = distroPlatform(taropts.Distro) ) - log.Printf("Validating rpm package for v%s-%s using redhat/ubi8:latest and platform %s\n", taropts.Version, taropts.Edition, taropts.Distro) + log.Printf("Validating rpm package for v%s%s using redhat/ubi8:latest and platform %s\n", taropts.Version, taropts.Suffix, taropts.Distro) // This grafana service runs in the background for the e2e tests service := d.Container(dagger.ContainerOpts{ @@ -199,7 +213,7 @@ func validateTarball(ctx context.Context, d *dagger.Client, pkg *dagger.File, sr archive = containers.ExtractedArchive(d, pkg, packageName) ) - log.Printf("Validating standalone tarball for v%s-%s using ubuntu:22.10 and platform %s\n", taropts.Version, taropts.Edition, taropts.Distro) + log.Printf("Validating standalone tarball for v%s%s using ubuntu:22.10 and platform %s\n", taropts.Version, taropts.Suffix, taropts.Distro) // This grafana service runs in the background for the e2e tests service := d.Container(dagger.ContainerOpts{ @@ -222,19 +236,144 @@ func validateTarball(ctx context.Context, d *dagger.Client, pkg *dagger.File, sr return containers.ValidatePackage(d, service, src, yarnCache, nodeVersion), nil } -// validateLicense uses the given service and license path to validate the license for each edition (enterprise or oss) +// validateLicense uses the given container and license path to validate the license for each edition (enterprise or oss) func validateLicense(ctx context.Context, service *dagger.Container, licensePath string, taropts TarFileOpts) error { license, err := service.File(licensePath).Contents(ctx) + if err != nil { + return err + } + if taropts.Edition == "enterprise" { - if err != nil || !strings.Contains(license, "Grafana Enterprise") { - return fmt.Errorf("failed to validate enterprise license") + if !strings.Contains(license, "Grafana Enterprise") { + return fmt.Errorf("license in package is not the Grafana Enterprise license agreement") } } if taropts.Edition == "" { - if err != nil || !strings.Contains(license, "GNU AFFERO GENERAL PUBLIC LICENSE") { - return fmt.Errorf("failed to validate open-source license") + if !strings.Contains(license, "GNU AFFERO GENERAL PUBLIC LICENSE") { + return fmt.Errorf("license in package is not the Grafana open-source license agreement") + } + } + + return nil +} + +// validateVersion uses the given container and version path to validate the version for each edition (enterprise or oss) +func validateVersion(ctx context.Context, service *dagger.Container, versionPath string, taropts TarFileOpts) error { + version, err := service.File(versionPath).Contents(ctx) + if err != nil { + return err + } + + if strings.TrimSpace(version) != taropts.Version { + return fmt.Errorf("version in package does not match version in package name") + } + + return nil +} + +// validateUpgrade verifies the extension of the first package and proceeds with upgrade validation for the same extension +func validateUpgrade(ctx context.Context, d *dagger.Client, packages []*dagger.File, names []string) error { + firstName := names[0] + if filepath.Ext(firstName) == ".deb" { + return validateDebUpgrade(ctx, d, packages, names) + } + + if strings.HasSuffix(firstName, ".rpm") { + return validateRpmUpgrade(ctx, d, packages, names) + } + + return fmt.Errorf("invalid upgrade package extension") +} + +// validateDebUpgrade receives a list of packages and package names, the names are used to retrieve information such as distro and edition +// the function expects all the packages to have the same distro, otherwise it outputs a distro mismatch error +// each package is installed to the same container and the license and version files are validated to see if the installation succeeded +func validateDebUpgrade(ctx context.Context, d *dagger.Client, packages []*dagger.File, names []string) error { + var lastopts *TarFileOpts + var container *dagger.Container + for i, name := range names { + if ext := filepath.Ext(name); ext != ".deb" { + return fmt.Errorf("expected a file ending in .deb, received '%s'", ext) } + + pkg := packages[i] + taropts := TarOptsFromFileName(name) + if container == nil { + container = d.Container(dagger.ContainerOpts{ + Platform: distroPlatform(taropts.Distro), + }).From("debian:latest"). + WithExec([]string{"apt-get", "update"}). + WithWorkdir("/usr/share/grafana") + } + + if lastopts != nil { + if lastopts.Distro != taropts.Distro { + return fmt.Errorf("upgrade package distro mismatch") + } + + log.Printf("Validating deb package upgrade from v%s%s to v%s%s using debian:latest and platform %s\n", lastopts.Version, lastopts.Suffix, taropts.Version, taropts.Suffix, lastopts.Distro) + } + + container = container. + WithFile("/src/package.deb", pkg). + WithExec([]string{"apt-get", "install", "-y", "/src/package.deb"}) + + if err := validateVersion(ctx, container, "/usr/share/grafana/VERSION", taropts); err != nil { + return err + } + + if err := validateLicense(ctx, container, "/usr/share/grafana/LICENSE", taropts); err != nil { + return err + } + + lastopts = &taropts + } + + return nil +} + +// validateRpmUpgrade receives a list of packages and package names, the names are used to retrieve information such as distro and edition +// the function expects all the packages to have the same distro, otherwise it outputs a distro mismatch error +// each package is installed to the same container and the license and version files are validated to see if the installation succeeded +func validateRpmUpgrade(ctx context.Context, d *dagger.Client, packages []*dagger.File, names []string) error { + var lastopts *TarFileOpts + var container *dagger.Container + for i, name := range names { + if ext := filepath.Ext(name); ext != ".rpm" { + return fmt.Errorf("expected a file ending in .rpm, received '%s'", ext) + } + + pkg := packages[i] + taropts := TarOptsFromFileName(name) + if container == nil { + container = d.Container(dagger.ContainerOpts{ + Platform: distroPlatform(taropts.Distro), + }).From("redhat/ubi8:latest"). + WithWorkdir("/usr/share/grafana") + } + + if lastopts != nil { + if lastopts.Distro != taropts.Distro { + return fmt.Errorf("upgrade package distro mismatch") + } + + log.Printf("Validating rpm package upgrade from v%s%s to v%s%s using redhat/ubi8:latest and platform %s\n", lastopts.Version, lastopts.Suffix, taropts.Version, taropts.Suffix, lastopts.Distro) + } + + container = container. + WithFile("/src/package.rpm", pkg). + WithExec([]string{"yum", "install", "-y", "--allowerasing", "/src/package.rpm"}) + + if err := validateVersion(ctx, container, "/usr/share/grafana/VERSION", taropts); err != nil { + return err + } + + if err := validateLicense(ctx, container, "/usr/share/grafana/LICENSE", taropts); err != nil { + return err + } + + lastopts = &taropts } return nil