Skip to content

Commit

Permalink
support for dirApp (#63)
Browse files Browse the repository at this point in the history
* support for dirApp
* implemented inferAppType
* updated docs
  • Loading branch information
ATGardner authored May 25, 2021
1 parent 31d3833 commit 7038573
Show file tree
Hide file tree
Showing 22 changed files with 2,239 additions and 886 deletions.
195 changes: 54 additions & 141 deletions cmd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"text/tabwriter"

"github.com/argoproj/argocd-autopilot/pkg/application"
Expand All @@ -18,15 +16,9 @@ import (
"github.com/argoproj/argocd-autopilot/pkg/util"

argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/ghodss/yaml"
"github.com/go-git/go-billy/v5/memfs"
billyUtils "github.com/go-git/go-billy/v5/util"
"github.com/spf13/cobra"
kusttypes "sigs.k8s.io/kustomize/api/types"
)

var (
ErrAppAlreadyInstalledOnProject = errors.New("application already installed on project")
ErrAppCollisionWithExistingBase = errors.New("an application with the same name and a different base already exists, consider choosing a different name")
)

type (
Expand Down Expand Up @@ -81,6 +73,9 @@ func NewAppCreateCommand(opts *BaseOptions) *cobra.Command {
--git-token <token> --repo <repo_url>
# using the --type flag (kustomize|dir) is optional. If it is ommitted, <BIN> will clone
# the --app repository, and infer the type automatically.
# Create a new application from kustomization in a remote repository
<BIN> app create <new_app_name> --app github.com/some_org/some_repo/manifests?ref=v1.2.3 --project project_name
Expand Down Expand Up @@ -111,25 +106,19 @@ func RunAppCreate(ctx context.Context, opts *AppCreateOptions) error {
return err
}

if opts.AppOpts.DestServer == store.Default.DestServer {
opts.AppOpts.DestServer, err = getProjectDestServer(repofs, opts.ProjectName)
if err != nil {
return err
}
}

if opts.AppOpts.DestNamespace == "" {
opts.AppOpts.DestNamespace = "default"
err = setAppOptsDefaults(ctx, repofs, opts)
if err != nil {
return err
}

app, err := opts.AppOpts.Parse()
app, err := opts.AppOpts.Parse(opts.ProjectName, opts.CloneOptions.URL, opts.CloneOptions.Revision)
if err != nil {
return fmt.Errorf("failed to parse application from flags: %v", err)
}

if err = createApplicationFiles(repofs, app, opts.ProjectName); err != nil {
if errors.Is(err, ErrAppAlreadyInstalledOnProject) {
return fmt.Errorf("application '%s' already exists in project '%s': %w", app.Name(), opts.ProjectName, ErrAppAlreadyInstalledOnProject)
if err = app.CreateFiles(repofs, opts.ProjectName); err != nil {
if errors.Is(err, application.ErrAppAlreadyInstalledOnProject) {
return fmt.Errorf("application '%s' already exists in project '%s': %w", app.Name(), opts.ProjectName, err)
}

return err
Expand All @@ -145,107 +134,50 @@ func RunAppCreate(ctx context.Context, opts *AppCreateOptions) error {
return nil
}

func createApplicationFiles(repoFS fs.FS, app application.Application, projectName string) error {
basePath := repoFS.Join(store.Default.KustomizeDir, app.Name(), "base")
overlayPath := repoFS.Join(store.Default.KustomizeDir, app.Name(), "overlays", projectName)
func setAppOptsDefaults(ctx context.Context, repofs fs.FS, opts *AppCreateOptions) error {
var err error

// create Base
baseKustomizationPath := repoFS.Join(basePath, "kustomization.yaml")
baseKustomizationYAML, err := yaml.Marshal(app.Base())
if err != nil {
return fmt.Errorf("failed to marshal app base kustomization: %w", err)
}

if exists, err := writeApplicationFile(repoFS, baseKustomizationPath, "base", baseKustomizationYAML); err != nil {
return err
} else if exists {
// check if the bases are the same
log.G().Debug("application base with the same name exists, checking for collisions")
if collision, err := checkBaseCollision(repoFS, baseKustomizationPath, app.Base()); err != nil {
if opts.AppOpts.DestServer == store.Default.DestServer || opts.AppOpts.DestServer == "" {
opts.AppOpts.DestServer, err = getProjectDestServer(repofs, opts.ProjectName)
if err != nil {
return err
} else if collision {
return ErrAppCollisionWithExistingBase
}
}

// create Overlay
overlayKustomizationPath := repoFS.Join(overlayPath, "kustomization.yaml")
overlayKustomizationYAML, err := yaml.Marshal(app.Overlay())
if err != nil {
return fmt.Errorf("failed to marshal app overlay kustomization: %w", err)
}

if exists, err := writeApplicationFile(repoFS, overlayKustomizationPath, "overlay", overlayKustomizationYAML); err != nil {
return err
} else if exists {
return ErrAppAlreadyInstalledOnProject
if opts.AppOpts.DestNamespace == "" {
opts.AppOpts.DestNamespace = "default"
}

// get manifests - only used in flat installation mode
if app.Manifests() != nil {
manifestsPath := repoFS.Join(basePath, "install.yaml")
if _, err = writeApplicationFile(repoFS, manifestsPath, "manifests", app.Manifests()); err != nil {
return err
if opts.AppOpts.AppType == "" {
host, orgRepo, p, gitRef, _, _, _ := util.ParseGitUrl(opts.AppOpts.AppSpecifier)
url := host + orgRepo
log.G().Infof("Cloning repo: '%s', to infer app type from path '%s'", url, p)
cloneOpts := &git.CloneOptions{
URL: url,
Revision: gitRef,
RepoRoot: p,
Auth: opts.CloneOptions.Auth,
}
}

// if we override the namespace we also need to write the namespace manifests next to the overlay
if app.Namespace() != nil {
nsPath := repoFS.Join(overlayPath, "namespace.yaml")
nsYAML, err := yaml.Marshal(app.Namespace())
_, fs, err := clone(ctx, cloneOpts, fs.Create(memfs.New()))
if err != nil {
return fmt.Errorf("failed to marshal app overlay namespace: %w", err)
}

if _, err = writeApplicationFile(repoFS, nsPath, "application namespace", nsYAML); err != nil {
return err
}
}

configPath := repoFS.Join(overlayPath, "config.json")
config, err := json.Marshal(app.Config())
if err != nil {
return fmt.Errorf("failed to marshal app config.json: %w", err)
}

if _, err = writeApplicationFile(repoFS, configPath, "config", config); err != nil {
return err
opts.AppOpts.AppType = application.InferAppType(fs)
log.G().Infof("Inferred AppType: %s", opts.AppOpts.AppType)
}

return nil
}

func checkBaseCollision(repoFS fs.FS, orgBasePath string, newBase *kusttypes.Kustomization) (bool, error) {
f, err := repoFS.Open(orgBasePath)
if err != nil {
return false, err
}

data, err := ioutil.ReadAll(f)
if err != nil {
return false, err
}

orgBase := &kusttypes.Kustomization{}
if err = yaml.Unmarshal(data, orgBase); err != nil {
return false, err
}

return !reflect.DeepEqual(orgBase, newBase), nil
}

func writeApplicationFile(repoFS fs.FS, path, name string, data []byte) (bool, error) {
absPath := repoFS.Join(repoFS.Root(), path)
exists, err := repoFS.CheckExistsOrWrite(path, data)
if err != nil {
return false, fmt.Errorf("failed to create '%s' file at '%s': %w", name, absPath, err)
} else if exists {
log.G().Infof("'%s' file exists in '%s'", name, absPath)
return true, nil
func getProjectDestServer(repofs fs.FS, projectName string) (string, error) {
path := repofs.Join(store.Default.ProjectsDir, projectName+".yaml")
p := &argocdv1alpha1.AppProject{}
if err := repofs.ReadYamls(path, p); err != nil {
return "", fmt.Errorf("failed to unmarshal project: %w", err)
}

log.G().Infof("created '%s' file at '%s'", name, absPath)
return false, nil
return p.Annotations[store.Default.DestServerAnnotation], nil
}

func getCommitMsg(opts *AppCreateOptions, repofs fs.FS) string {
Expand All @@ -257,25 +189,6 @@ func getCommitMsg(opts *AppCreateOptions, repofs fs.FS) string {
return commitMsg
}

var getProjectDestServer = func(repofs fs.FS, projectName string) (string, error) {
f, err := repofs.Open(repofs.Join(store.Default.ProjectsDir, projectName+".yaml"))
if err != nil {
return "", err
}

d, err := ioutil.ReadAll(f)
if err != nil {
return "", fmt.Errorf("failed to read namespace file: %w", err)
}

p := &argocdv1alpha1.AppProject{}
if err = yaml.Unmarshal(d, p); err != nil {
return "", fmt.Errorf("failed to unmarshal project: %w", err)
}

return p.Annotations[store.Default.DestServerAnnotation], nil
}

func NewAppListCommand(opts *BaseOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "list [PROJECT_NAME]",
Expand Down Expand Up @@ -314,17 +227,17 @@ func RunAppList(ctx context.Context, opts *BaseOptions) error {
return err
}

// get all apps beneath kustomize <project>\overlayes
matches, err := billyUtils.Glob(repofs, repofs.Join(store.Default.KustomizeDir, "*", store.Default.OverlaysDir, opts.ProjectName))
// get all apps beneath apps/*/overlays/<project>
matches, err := billyUtils.Glob(repofs, repofs.Join(store.Default.AppsDir, "*", store.Default.OverlaysDir, opts.ProjectName))
if err != nil {
log.G().Fatalf("failed to run glob on %s", opts.ProjectName)
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "PROJECT\tNAME\tDEST_NAMESPACE\tDEST_SERVER\t\n")

for _, appName := range matches {
conf, err := getConfigFileFromPath(repofs, appName)
for _, appPath := range matches {
conf, err := getConfigFileFromPath(repofs, appPath)
if err != nil {
return err
}
Expand All @@ -336,22 +249,17 @@ func RunAppList(ctx context.Context, opts *BaseOptions) error {
return nil
}

func getConfigFileFromPath(fs fs.FS, appName string) (*application.Config, error) {
confFileName := fmt.Sprintf("%s/config.json", appName)
file, err := fs.Open(confFileName)
if err != nil {
return nil, fmt.Errorf("%s not found", confFileName)
}

b, err := ioutil.ReadAll(file)
func getConfigFileFromPath(repofs fs.FS, appPath string) (*application.Config, error) {
path := repofs.Join(appPath, "config.json")
b, err := repofs.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file %s", confFileName)
return nil, fmt.Errorf("failed to read file '%s'", path)
}

conf := application.Config{}
err = json.Unmarshal(b, &conf)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal file %s", confFileName)
return nil, fmt.Errorf("failed to unmarshal file '%s'", path)
}

return &conf, nil
Expand Down Expand Up @@ -411,7 +319,7 @@ func RunAppDelete(ctx context.Context, opts *AppDeleteOptions) error {
return err
}

appDir := repofs.Join(store.Default.KustomizeDir, opts.AppName)
appDir := repofs.Join(store.Default.AppsDir, opts.AppName)
appExists := repofs.ExistsOrDie(appDir)
if !appExists {
return fmt.Errorf(util.Doc(fmt.Sprintf("application '%s' not found", opts.AppName)))
Expand All @@ -423,8 +331,13 @@ func RunAppDelete(ctx context.Context, opts *AppDeleteOptions) error {
dirToRemove = appDir
} else {
appOverlaysDir := repofs.Join(appDir, store.Default.OverlaysDir)
projectDir := repofs.Join(appOverlaysDir, opts.ProjectName)
overlayExists := repofs.ExistsOrDie(projectDir)
overlaysExists := repofs.ExistsOrDie(appOverlaysDir)
if !overlaysExists {
appOverlaysDir = appDir
}

appProjectDir := repofs.Join(appOverlaysDir, opts.ProjectName)
overlayExists := repofs.ExistsOrDie(appProjectDir)
if !overlayExists {
return fmt.Errorf(util.Doc(fmt.Sprintf("application '%s' not found in project '%s'", opts.AppName, opts.ProjectName)))
}
Expand All @@ -438,7 +351,7 @@ func RunAppDelete(ctx context.Context, opts *AppDeleteOptions) error {
dirToRemove = appDir
} else {
commitMsg += fmt.Sprintf(" from project '%s'", opts.ProjectName)
dirToRemove = projectDir
dirToRemove = appProjectDir
}
}

Expand Down
Loading

0 comments on commit 7038573

Please sign in to comment.