Skip to content

Commit

Permalink
feat: package file validation (#155)
Browse files Browse the repository at this point in the history
Fixes #135
  • Loading branch information
agaffney authored Mar 20, 2024
1 parent 41abc81 commit c5a4717
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 40 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/package-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: package-validate

on:
push:
tags:
- v*
branches:
- main
pull_request:

jobs:
validate:
name: Validate packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.x
- run: make
- run: ./cardano-up validate -D
10 changes: 1 addition & 9 deletions cmd/cardano-up/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,6 @@ func main() {

rootCmd := &cobra.Command{
Use: programName,
/*
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
*/
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Configure default logger
logLevel := slog.LevelInfo
Expand Down Expand Up @@ -77,6 +68,7 @@ func main() {
downCommand(),
updateCommand(),
upgradeCommand(),
validateCommand(),
)

if err := rootCmd.Execute(); err != nil {
Expand Down
76 changes: 76 additions & 0 deletions cmd/cardano-up/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"

"github.com/blinklabs-io/cardano-up/pkgmgr"
"github.com/spf13/cobra"
)

func validateCommand() *cobra.Command {
validateCmd := &cobra.Command{
Use: "validate [path]",
Short: "Validate package file(s) in the given directory",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
return errors.New("only one package directory may be specified at a time")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
packagesDir := "."
if len(args) > 0 {
packagesDir = args[0]
}
absPackagesDir, err := filepath.Abs(packagesDir)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
cfg, err := pkgmgr.NewDefaultConfig()
if err != nil {
slog.Error(fmt.Sprintf("failed to create package manager: %s", err))
os.Exit(1)
}
// Point at provided registry dir
cfg.RegistryDir = absPackagesDir
// Disable preloading of registry to prevent errors before we explicitly start validation
cfg.RegistryPreload = false
pm, err := pkgmgr.NewPackageManager(cfg)
if err != nil {
slog.Error(fmt.Sprintf("failed to create package manager: %s", err))
os.Exit(1)
}
slog.Info(
fmt.Sprintf(
"Validating packages in path %s",
absPackagesDir,
),
)
if err := pm.ValidatePackages(); err != nil {
slog.Error("problems were found")
os.Exit(1)
}
slog.Info("No problems found!")
},
}
return validateCmd
}
4 changes: 3 additions & 1 deletion pkgmgr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Config struct {
RequiredPackageTags []string
RegistryUrl string
RegistryDir string
RegistryPreload bool
}

func NewDefaultConfig() (Config, error) {
Expand Down Expand Up @@ -79,7 +80,8 @@ func NewDefaultConfig() (Config, error) {
runtime.GOOS,
runtime.GOARCH,
},
RegistryUrl: "https://github.com/blinklabs-io/cardano-up/archive/refs/heads/main.zip",
RegistryUrl: "https://github.com/blinklabs-io/cardano-up/archive/refs/heads/main.zip",
RegistryPreload: true,
}
return ret, nil
}
3 changes: 3 additions & 0 deletions pkgmgr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ var ErrContainerNotExists = errors.New("specified container does not exist")
// ErrNoRegistryConfigured is returned when no registry is configured
var ErrNoRegistryConfigured = errors.New("no package registry is configured")

// ErrValidationFailed is returned when loading the package registry while doing package validation when a package failed to load
var ErrValidationFailed = errors.New("validation failed")

func NewUnknownNetworkError(networkName string) error {
return fmt.Errorf(
"unknown network %q",
Expand Down
76 changes: 71 additions & 5 deletions pkgmgr/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"regexp"
"strings"

"github.com/hashicorp/go-version"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -64,11 +65,9 @@ func NewPackageFromFile(path string) (Package, error) {

func NewPackageFromReader(r io.Reader) (Package, error) {
var ret Package
data, err := io.ReadAll(r)
if err != nil {
return Package{}, err
}
if err := yaml.Unmarshal(data, &ret); err != nil {
dec := yaml.NewDecoder(r)
dec.KnownFields(true)
if err := dec.Decode(&ret); err != nil {
return Package{}, err
}
return ret, nil
Expand Down Expand Up @@ -397,6 +396,60 @@ func (p Package) deactivate(cfg Config, context string) error {
}
return nil
}

func (p Package) validate(cfg Config) error {
// Check empty name
if p.Name == "" {
return fmt.Errorf("package name cannot be empty")
}
// Check name matches allowed characters
reName := regexp.MustCompile(`^[-a-zA-Z0-9]+$`)
if !reName.Match([]byte(p.Name)) {
return fmt.Errorf("invalid package name: %s", p.Name)
}
// Check empty version
if p.Version == "" {
return fmt.Errorf("package version cannot be empty")
}
// Check version is well formed
if _, err := version.NewVersion(p.Version); err != nil {
return fmt.Errorf("package version is malformed: %s", err)
}
// Check if package path matches package name/version
expectedFilePath := filepath.Join(
p.Name,
fmt.Sprintf(
"%s-%s.yaml",
p.Name,
p.Version,
),
)
if !strings.HasSuffix(p.filePath, expectedFilePath) {
return fmt.Errorf("package did not have expected file path: %s", expectedFilePath)
}
// Validate install steps
for _, installStep := range p.InstallSteps {
// Evaluate condition if defined
if installStep.Condition != "" {
if _, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil {
return NewInstallStepConditionError(installStep.Condition, err)
}
}
if installStep.Docker != nil {
if err := installStep.Docker.validate(cfg); err != nil {
return err
}
} else if installStep.File != nil {
if err := installStep.File.validate(cfg); err != nil {
return err
}
} else {
return ErrNoInstallMethods
}
}
return nil
}

func (p Package) startService(cfg Config, context string) error {
pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context)

Expand Down Expand Up @@ -498,6 +551,14 @@ type PackageInstallStepDocker struct {
PullOnly bool `yaml:"pullOnly"`
}

func (p *PackageInstallStepDocker) validate(cfg Config) error {
if p.Image == "" {
return fmt.Errorf("docker image must be provided")
}
// TODO: add more checks
return nil
}

func (p *PackageInstallStepDocker) preflight(cfg Config, pkgName string) error {
if err := CheckDockerConnectivity(); err != nil {
return err
Expand Down Expand Up @@ -674,6 +735,11 @@ type PackageInstallStepFile struct {
Mode fs.FileMode `yaml:"mode,omitempty"`
}

func (p *PackageInstallStepFile) validate(cfg Config) error {
// TODO: add checks
return nil
}

func (p *PackageInstallStepFile) install(cfg Config, pkgName string, packagePath string) error {
tmpFilePath, err := cfg.Template.Render(p.Filename, nil)
if err != nil {
Expand Down
66 changes: 56 additions & 10 deletions pkgmgr/pkgmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ func NewDefaultPackageManager() (*PackageManager, error) {
}

func (p *PackageManager) init() error {
p.config.Logger.Debug("initializing package manager")
if err := p.state.Load(); err != nil {
return fmt.Errorf("failed to load state: %s", err)
}
// Get available packages from configured registry
if err := p.loadPackageRegistry(); err != nil {
return err
if p.config.RegistryPreload {
if err := p.loadPackageRegistry(false); err != nil {
return err
}
}
// Setup templating
p.initTemplate()
Expand All @@ -87,13 +88,19 @@ func (p *PackageManager) initTemplate() {
p.config = tmpConfig
}

func (p *PackageManager) loadPackageRegistry() error {
if registryPkgs, err := registryPackages(p.config); err != nil {
return err
} else {
p.availablePackages = registryPkgs[:]
func (p *PackageManager) loadPackageRegistry(validate bool) error {
var retErr error
registryPkgs, err := registryPackages(p.config, validate)
if err != nil {
if err == ErrValidationFailed {
// We want to pass along the validation error, but only after we record the packages
retErr = err
} else {
return err
}
}
return nil
p.availablePackages = registryPkgs[:]
return retErr
}

func (p *PackageManager) AvailablePackages() []Package {
Expand Down Expand Up @@ -631,9 +638,48 @@ func (p *PackageManager) UpdatePackages() error {
return err
}
// (Re)load the package registry
if err := p.loadPackageRegistry(); err != nil {
if err := p.loadPackageRegistry(false); err != nil {
return err
}
return nil
}

func (p *PackageManager) ValidatePackages() error {
foundError := false
if len(p.availablePackages) == 0 {
if err := p.loadPackageRegistry(true); err != nil {
if err == ErrValidationFailed {
// Record error for later failure
// The error(s) will have already been output to the console
foundError = true
} else {
return err
}
}
}
for _, pkg := range p.availablePackages {
if pkg.filePath == "" {
continue
}
p.config.Logger.Debug(
fmt.Sprintf(
"checking package %s",
pkg.filePath,
),
)
if err := pkg.validate(p.config); err != nil {
foundError = true
p.config.Logger.Warn(
fmt.Sprintf(
"validation failed: %s: %s",
pkg.filePath,
err.Error(),
),
)
}
}
if foundError {
return ErrOperationFailed
}
return nil
}
Loading

0 comments on commit c5a4717

Please sign in to comment.