Compare commits
	
		
			37 Commits
		
	
	
		
			0.3.1-alph
			...
			app-error
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ed859c0243 | |||
| 236d0f5892 | |||
| 6c87d501e6 | |||
| 930c29f4a2 | |||
| 1d6c3e98e4 | |||
| a90f3b7463 | |||
| 962f566228 | |||
| 9896c57399 | |||
| 748d607ddc | |||
| 3901258a96 | |||
| 4347083f98 | |||
| 4641a942d8 | |||
| 759a00eeb3 | |||
| d1526fad21 | |||
| 6ef15e0a26 | |||
| dd0f328a65 | |||
| aea5cc69c3 | |||
| b02475eca5 | |||
| d0a30f6b7b | |||
| 8635922b9f | |||
| 9d62fff074 | |||
| 711c4e5ee8 | |||
| cb32e88cde | |||
| a18729bf98 | |||
| dbf84b7640 | |||
| 75db249053 | |||
| fdf4fc6737 | |||
| ef6a9abba9 | |||
| ce57d5ed54 | |||
| 3b01b1bb2e | |||
| fbdb792795 | |||
| 900f40f07a | |||
| ecd2a63f0a | |||
| 304b70639f | |||
| d821975aa2 | |||
| 1b836dbab6 | |||
| fc51cf7775 | 
| @ -6,6 +6,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||||
|  | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/catalogue" | 	"coopcloud.tech/abra/pkg/catalogue" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/ssh" | 	"coopcloud.tech/abra/pkg/ssh" | ||||||
| @ -70,14 +71,18 @@ can take some time. | |||||||
| 		} | 		} | ||||||
| 		sort.Sort(config.ByServerAndType(apps)) | 		sort.Sort(config.ByServerAndType(apps)) | ||||||
|  |  | ||||||
|  | 		alreadySeen := make(map[string]bool) | ||||||
| 		for _, app := range apps { | 		for _, app := range apps { | ||||||
|  | 			if _, ok := alreadySeen[app.Server]; !ok { | ||||||
| 				if err := ssh.EnsureHostKey(app.Server); err != nil { | 				if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||||
| 				logrus.Fatal(err) | 					logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server)) | ||||||
|  | 				} | ||||||
|  | 				alreadySeen[app.Server] = true | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		statuses := make(map[string]map[string]string) | 		statuses := make(map[string]map[string]string) | ||||||
| 		tableCol := []string{"Server", "Type", "Domain"} | 		tableCol := []string{"Server", "Type", "App Name", "Domain"} | ||||||
| 		if status { | 		if status { | ||||||
| 			tableCol = append(tableCol, "Status", "Version", "Updates") | 			tableCol = append(tableCol, "Status", "Version", "Updates") | ||||||
| 			statuses, err = config.GetAppStatuses(appFiles) | 			statuses, err = config.GetAppStatuses(appFiles) | ||||||
| @ -96,11 +101,19 @@ can take some time. | |||||||
| 			canUpgradeCount      int | 			canUpgradeCount      int | ||||||
| 		) | 		) | ||||||
|  |  | ||||||
|  | 		catl, err := catalogue.ReadRecipeCatalogue() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var appsCount int | ||||||
| 		for _, app := range apps { | 		for _, app := range apps { | ||||||
| 			var tableRow []string | 			var tableRow []string | ||||||
| 			if app.Type == appType || appType == "" { | 			if app.Type == appType || appType == "" { | ||||||
|  | 				appsCount++ | ||||||
|  |  | ||||||
| 				// If type flag is set, check for it, if not, Type == "" | 				// If type flag is set, check for it, if not, Type == "" | ||||||
| 				tableRow = []string{app.Server, app.Type, app.Domain} | 				tableRow = []string{app.Server, app.Type, app.StackName(), app.Domain} | ||||||
| 				if status { | 				if status { | ||||||
| 					stackName := app.StackName() | 					stackName := app.StackName() | ||||||
| 					status := "unknown" | 					status := "unknown" | ||||||
| @ -121,7 +134,8 @@ can take some time. | |||||||
|  |  | ||||||
| 					var newUpdates []string | 					var newUpdates []string | ||||||
| 					if version != "unknown" { | 					if version != "unknown" { | ||||||
| 						updates, err := catalogue.GetRecipeCatalogueVersions(app.Type) |  | ||||||
|  | 						updates, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl) | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							logrus.Fatal(err) | 							logrus.Fatal(err) | ||||||
| 						} | 						} | ||||||
| @ -163,14 +177,19 @@ can take some time. | |||||||
| 			table.Append(tableRow) | 			table.Append(tableRow) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		stats := fmt.Sprintf( | 		var stats string | ||||||
|  | 		if status { | ||||||
|  | 			stats = fmt.Sprintf( | ||||||
| 				"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v", | 				"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v", | ||||||
| 			len(apps), | 				appsCount, | ||||||
| 				versionedAppsCount, | 				versionedAppsCount, | ||||||
| 				unversionedAppsCount, | 				unversionedAppsCount, | ||||||
| 				onLatestCount, | 				onLatestCount, | ||||||
| 				canUpgradeCount, | 				canUpgradeCount, | ||||||
| 			) | 			) | ||||||
|  | 		} else { | ||||||
|  | 			stats = fmt.Sprintf("Total apps: %v", appsCount) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		table.SetCaption(true, stats) | 		table.SetCaption(true, stats) | ||||||
| 		table.Render() | 		table.Render() | ||||||
|  | |||||||
| @ -4,74 +4,54 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"sync" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerClient "github.com/docker/docker/client" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // stackLogs lists logs for all stack services |  | ||||||
| func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) { |  | ||||||
| 	filters := filters.NewArgs() |  | ||||||
| 	filters.Add("name", stackName) |  | ||||||
| 	serviceOpts := types.ServiceListOptions{Filters: filters} |  | ||||||
| 	services, err := client.ServiceList(c.Context, serviceOpts) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logrus.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var wg sync.WaitGroup |  | ||||||
| 	for _, service := range services { |  | ||||||
| 		wg.Add(1) |  | ||||||
| 		go func(s string) { |  | ||||||
| 			logOpts := types.ContainerLogsOptions{ |  | ||||||
| 				Details:    true, |  | ||||||
| 				Follow:     true, |  | ||||||
| 				ShowStderr: true, |  | ||||||
| 				ShowStdout: true, |  | ||||||
| 				Tail:       "20", |  | ||||||
| 				Timestamps: true, |  | ||||||
| 			} |  | ||||||
| 			logs, err := client.ServiceLogs(c.Context, s, logOpts) |  | ||||||
| 			if err != nil { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 			// defer after err check as any err returns a nil io.ReadCloser |  | ||||||
| 			defer logs.Close() |  | ||||||
|  |  | ||||||
| 			_, err = io.Copy(os.Stdout, logs) |  | ||||||
| 			if err != nil && err != io.EOF { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 		}(service.ID) |  | ||||||
| 	} |  | ||||||
| 	wg.Wait() |  | ||||||
| 	os.Exit(0) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var appLogsCommand = &cli.Command{ | var appLogsCommand = &cli.Command{ | ||||||
| 	Name:      "logs", | 	Name:      "logs", | ||||||
| 	Aliases:   []string{"l"}, | 	Aliases:   []string{"l"}, | ||||||
| 	ArgsUsage: "[<service>]", | 	ArgsUsage: "[<service>]", | ||||||
| 	Usage:     "Tail app logs", | 	Usage:     "Tail app logs", | ||||||
|  | 	Flags: []cli.Flag{ | ||||||
|  | 		internal.StderrFlag, | ||||||
|  | 		internal.StdoutFlag, | ||||||
|  | 		internal.HealthcheckFlag, | ||||||
|  | 	}, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
|  | 		if !internal.Stderr && !internal.Stdout && !internal.Healthcheck { | ||||||
|  | 			internal.Stderr = true | ||||||
|  | 			internal.Stdout = true | ||||||
|  | 			internal.Healthcheck = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		logrus.Debugf("flags parsed. --stderr: %t, --stdout: %t, --healthcheck: %t", internal.Stderr, internal.Stdout, internal.Healthcheck) | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		logOpts := types.ContainerLogsOptions{ | ||||||
|  | 			Details:    false, | ||||||
|  | 			Follow:     true, | ||||||
|  | 			ShowStderr: internal.Stderr, | ||||||
|  | 			ShowStdout: internal.Stdout, | ||||||
|  | 			Tail:       "20", | ||||||
|  | 			Timestamps: true, | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		serviceName := c.Args().Get(1) | 		serviceName := c.Args().Get(1) | ||||||
| 		if serviceName == "" { | 		if serviceName == "" { | ||||||
| 			logrus.Debug("tailing logs for all app services") | 			logrus.Debug("tailing logs for all app services") | ||||||
| 			stackLogs(c, app.StackName(), cl) | 			internal.StackLogs(c, app.StackName(), logOpts, cl) | ||||||
| 		} | 		} | ||||||
| 		logrus.Debugf("tailing logs for '%s'", serviceName) | 		logrus.Debugf("tailing logs for '%s'", serviceName) | ||||||
|  |  | ||||||
| @ -87,14 +67,6 @@ var appLogsCommand = &cli.Command{ | |||||||
| 			logrus.Fatalf("expected 1 service but got %v", len(services)) | 			logrus.Fatalf("expected 1 service but got %v", len(services)) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		logOpts := types.ContainerLogsOptions{ |  | ||||||
| 			Details:    true, |  | ||||||
| 			Follow:     true, |  | ||||||
| 			ShowStderr: true, |  | ||||||
| 			ShowStdout: true, |  | ||||||
| 			Tail:       "20", |  | ||||||
| 			Timestamps: true, |  | ||||||
| 		} |  | ||||||
| 		logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts) | 		logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
|  | |||||||
| @ -28,6 +28,7 @@ var watchFlag = &cli.BoolFlag{ | |||||||
| var appPsCommand = &cli.Command{ | var appPsCommand = &cli.Command{ | ||||||
| 	Name:        "ps", | 	Name:        "ps", | ||||||
| 	Usage:       "Check app status", | 	Usage:       "Check app status", | ||||||
|  | 	Description: "This command shows a more detailed status output of a specific deployed app.", | ||||||
| 	Aliases:     []string{"p"}, | 	Aliases:     []string{"p"}, | ||||||
| 	Flags: []cli.Flag{ | 	Flags: []cli.Flag{ | ||||||
| 		watchFlag, | 		watchFlag, | ||||||
| @ -75,7 +76,7 @@ func showPSOutput(c *cli.Context) { | |||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	tableCol := []string{"image", "created", "status", "ports", "names"} | 	tableCol := []string{"image", "created", "status", "ports", "app name", "services"} | ||||||
| 	table := abraFormatter.CreateTable(tableCol) | 	table := abraFormatter.CreateTable(tableCol) | ||||||
|  |  | ||||||
| 	for _, container := range containers { | 	for _, container := range containers { | ||||||
| @ -90,6 +91,7 @@ func showPSOutput(c *cli.Context) { | |||||||
| 			abraFormatter.HumanDuration(container.Created), | 			abraFormatter.HumanDuration(container.Created), | ||||||
| 			container.Status, | 			container.Status, | ||||||
| 			formatter.DisplayablePorts(container.Ports), | 			formatter.DisplayablePorts(container.Ports), | ||||||
|  | 			app.StackName(), | ||||||
| 			strings.Join(containerNames, "\n"), | 			strings.Join(containerNames, "\n"), | ||||||
| 		} | 		} | ||||||
| 		table.Append(tableRow) | 		table.Append(tableRow) | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
|  | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| @ -48,23 +49,18 @@ var appRemoveCommand = &cli.Command{ | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		appFiles, err := config.LoadAppFiles("") |  | ||||||
| 		if err != nil { |  | ||||||
| 			logrus.Fatal(err) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Force { | 		if !internal.Force { | ||||||
| 			// FIXME: only query for app we are interested in, not all of them! | 			isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) | ||||||
| 			statuses, err := config.GetAppStatuses(appFiles) |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			if statuses[app.Name]["status"] == "deployed" { | 			if isDeployed { | ||||||
| 				logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name) | 				logrus.Fatalf("'%s' is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | |||||||
| @ -70,7 +70,12 @@ recipes. | |||||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | 		catl, err := catalogue.ReadRecipeCatalogue() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -62,7 +62,12 @@ recipes. | |||||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | 		catl, err := catalogue.ReadRecipeCatalogue() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -56,7 +56,7 @@ Example: | |||||||
|  |  | ||||||
| Supported shells are as follows: | Supported shells are as follows: | ||||||
|  |  | ||||||
| 		fish | 		fizsh | ||||||
| 		zsh | 		zsh | ||||||
|     bash |     bash | ||||||
| `, | `, | ||||||
| @ -71,14 +71,14 @@ Supported shells are as follows: | |||||||
| 		supportedShells := map[string]bool{ | 		supportedShells := map[string]bool{ | ||||||
| 			"bash":  true, | 			"bash":  true, | ||||||
| 			"zsh":   true, | 			"zsh":   true, | ||||||
| 			"fish": true, | 			"fizsh": true, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if _, ok := supportedShells[shellType]; !ok { | 		if _, ok := supportedShells[shellType]; !ok { | ||||||
| 			logrus.Fatalf("%s is not a supported shell right now, sorry", shellType) | 			logrus.Fatalf("%s is not a supported shell right now, sorry", shellType) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if shellType == "fish" { | 		if shellType == "fizsh" { | ||||||
| 			shellType = "zsh" // handled the same on the autocompletion side | 			shellType = "zsh" // handled the same on the autocompletion side | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | |||||||
| @ -40,15 +40,18 @@ var CatalogueSkipList = map[string]bool{ | |||||||
| 	"docker-cp-deploy":      true, | 	"docker-cp-deploy":      true, | ||||||
| 	"docker-dind-bats-kcov": true, | 	"docker-dind-bats-kcov": true, | ||||||
| 	"docs.coopcloud.tech":   true, | 	"docs.coopcloud.tech":   true, | ||||||
|  | 	"drone-abra":            true, | ||||||
| 	"example":               true, | 	"example":               true, | ||||||
| 	"gardening":             true, | 	"gardening":             true, | ||||||
| 	"go-abra":               true, | 	"go-abra":               true, | ||||||
| 	"organising":            true, | 	"organising":            true, | ||||||
| 	"pyabra":                true, | 	"pyabra":                true, | ||||||
| 	"radicle-seed-node":     true, | 	"radicle-seed-node":     true, | ||||||
|  | 	"recipes":               true, | ||||||
| 	"stack-ssh-deploy":      true, | 	"stack-ssh-deploy":      true, | ||||||
| 	"swarm-cronjob":         true, | 	"swarm-cronjob":         true, | ||||||
| 	"tagcmp":                true, | 	"tagcmp":                true, | ||||||
|  | 	"traefik-cert-dumper":   true, | ||||||
| 	"tyop":                  true, | 	"tyop":                  true, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -91,6 +94,9 @@ A new catalogue copy can be published to the recipes repository by passing the | |||||||
| 	ArgsUsage: "[<recipe>]", | 	ArgsUsage: "[<recipe>]", | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipeName := c.Args().First() | 		recipeName := c.Args().First() | ||||||
|  | 		if recipeName != "" { | ||||||
|  | 			internal.ValidateRecipe(c) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | 		catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | ||||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | ||||||
| @ -130,6 +136,15 @@ A new catalogue copy can be published to the recipes repository by passing the | |||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | 				isClean, err := gitPkg.IsClean(rm.Name) | ||||||
|  | 				if err != nil { | ||||||
|  | 					logrus.Fatal(err) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if !isClean { | ||||||
|  | 					logrus.Fatalf("'%s' has locally unstaged changes", rm.Name) | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 				if err := gitPkg.EnsureUpToDate(recipeDir); err != nil { | 				if err := gitPkg.EnsureUpToDate(recipeDir); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| @ -161,6 +176,11 @@ A new catalogue copy can be published to the recipes repository by passing the | |||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			features, category, err := catalogue.GetRecipeFeaturesAndCategory(recipeMeta.Name) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Warn(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			catl[recipeMeta.Name] = catalogue.RecipeMeta{ | 			catl[recipeMeta.Name] = catalogue.RecipeMeta{ | ||||||
| 				Name:          recipeMeta.Name, | 				Name:          recipeMeta.Name, | ||||||
| 				Repository:    recipeMeta.CloneURL, | 				Repository:    recipeMeta.CloneURL, | ||||||
| @ -169,8 +189,8 @@ A new catalogue copy can be published to the recipes repository by passing the | |||||||
| 				Description:   recipeMeta.Description, | 				Description:   recipeMeta.Description, | ||||||
| 				Website:       recipeMeta.Website, | 				Website:       recipeMeta.Website, | ||||||
| 				Versions:      versions, | 				Versions:      versions, | ||||||
| 				// Category:      ..., // FIXME: parse & load | 				Category:      category, | ||||||
| 				// Features:      ..., // FIXME: parse & load | 				Features:      features, | ||||||
| 			} | 			} | ||||||
| 			catlBar.Add(1) | 			catlBar.Add(1) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -271,3 +271,48 @@ var DebugFlag = &cli.BoolFlag{ | |||||||
| 	Destination: &Debug, | 	Destination: &Debug, | ||||||
| 	Usage:       "Show DEBUG messages", | 	Usage:       "Show DEBUG messages", | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // SSHFailMsg is a hopefully helpful SSH failure message | ||||||
|  | var SSHFailMsg = ` | ||||||
|  | Woops, Abra is unable to connect to connect to %s. | ||||||
|  |  | ||||||
|  | Here are a few tips for debugging your local SSH config. Abra uses plain 'ol | ||||||
|  | SSH to make connections to servers, so if your SSH config is working, Abra is | ||||||
|  | working. | ||||||
|  |  | ||||||
|  | In the first place, Abra will always try to read your Docker context connection | ||||||
|  | string for SSH connection details. You can view your server context configs | ||||||
|  | with the following command. Are they correct? | ||||||
|  |  | ||||||
|  |     abra server ls | ||||||
|  |  | ||||||
|  | Is your ssh-agent running? You can start it by running the following command: | ||||||
|  |  | ||||||
|  |     eval "$(ssh-agent)" | ||||||
|  |  | ||||||
|  | If your SSH private key loaded? You can check by running the following command: | ||||||
|  |  | ||||||
|  |     ssh-add -L | ||||||
|  |  | ||||||
|  | If you are using a non-default public/private key, you can configure this in | ||||||
|  | your ~/.ssh/config file which Abra will read in order to figure out connection | ||||||
|  | details: | ||||||
|  |  | ||||||
|  | Host foo.coopcloud.tech | ||||||
|  |   Hostname foo.coopcloud.tech | ||||||
|  |   User bar | ||||||
|  |   Port 12345 | ||||||
|  |   IdentityFile ~/.ssh/bar@foo.coopcloud.tech | ||||||
|  |  | ||||||
|  | If you're only using password authentication, you can use the following config: | ||||||
|  |  | ||||||
|  | Host foo.coopcloud.tech | ||||||
|  |   Hostname foo.coopcloud.tech | ||||||
|  |   User bar | ||||||
|  |   Port 12345 | ||||||
|  |   PreferredAuthentications=password | ||||||
|  |   PubkeyAuthentication=no | ||||||
|  |  | ||||||
|  | Good luck! | ||||||
|  |  | ||||||
|  | ` | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ func DeployAction(c *cli.Context) error { | |||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("checking whether '%s' is already deployed", stackName) | 	logrus.Debugf("checking whether %s is already deployed", stackName) | ||||||
|  |  | ||||||
| 	isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | 	isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -34,24 +34,26 @@ func DeployAction(c *cli.Context) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if isDeployed { | 	if isDeployed { | ||||||
| 		if Force { | 		if Force || Chaos { | ||||||
| 			logrus.Warnf("'%s' already deployed but continuing (--force)", stackName) | 			logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", stackName) | ||||||
| 		} else if Chaos { |  | ||||||
| 			logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName) |  | ||||||
| 		} else { | 		} else { | ||||||
| 			logrus.Fatalf("'%s' is already deployed", stackName) | 			logrus.Fatalf("%s is already deployed", stackName) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	version := deployedVersion | 	version := deployedVersion | ||||||
| 	if version == "" && !Chaos { | 	if version == "" && !Chaos { | ||||||
| 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | 		catl, err := catalogue.ReadRecipeCatalogue() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
| 		if len(versions) > 0 { | 		if len(versions) > 0 { | ||||||
| 			version = versions[len(versions)-1] | 			version = versions[len(versions)-1] | ||||||
| 			logrus.Debugf("choosing '%s' as version to deploy", version) | 			logrus.Debugf("choosing %s as version to deploy", version) | ||||||
| 			if err := recipe.EnsureVersion(app.Type, version); err != nil { | 			if err := recipe.EnsureVersion(app.Type, version); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -65,7 +67,13 @@ func DeployAction(c *cli.Context) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if version == "" && !Chaos { | 	if version == "" && !Chaos { | ||||||
| 		logrus.Debugf("choosing '%s' as version to deploy", version) | 		logrus.Debugf("choosing %s as version to deploy", version) | ||||||
|  | 		if err := recipe.EnsureVersion(app.Type, version); err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if version != "" && !Chaos { | ||||||
| 		if err := recipe.EnsureVersion(app.Type, version); err != nil { | 		if err := recipe.EnsureVersion(app.Type, version); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								cli/internal/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								cli/internal/logs.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | package internal | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"github.com/docker/docker/api/types" | ||||||
|  | 	"github.com/docker/docker/api/types/filters" | ||||||
|  | 	dockerClient "github.com/docker/docker/client" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/urfave/cli/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var Stderr bool | ||||||
|  | var StderrFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "stderr", | ||||||
|  | 	Aliases:     []string{"e"}, | ||||||
|  | 	Value:       false, | ||||||
|  | 	Destination: &Stderr, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var Stdout bool | ||||||
|  | var StdoutFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "stdout", | ||||||
|  | 	Aliases:     []string{"o"}, | ||||||
|  | 	Value:       false, | ||||||
|  | 	Destination: &Stdout, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var Healthcheck bool | ||||||
|  | var HealthcheckFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "healthcheck", | ||||||
|  | 	Aliases:     []string{"c"}, | ||||||
|  | 	Value:       false, | ||||||
|  | 	Destination: &Healthcheck, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StackLogs lists logs for all stack services | ||||||
|  | func StackLogs(c *cli.Context, stackName string, logOpts types.ContainerLogsOptions, client *dockerClient.Client) { | ||||||
|  | 	filters := filters.NewArgs() | ||||||
|  | 	filters.Add("name", stackName) | ||||||
|  | 	serviceOpts := types.ServiceListOptions{Filters: filters} | ||||||
|  | 	services, err := client.ServiceList(c.Context, serviceOpts) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var wg sync.WaitGroup | ||||||
|  | 	for _, service := range services { | ||||||
|  | 		wg.Add(1) | ||||||
|  | 		go func(s string) { | ||||||
|  | 			logs, err := client.ServiceLogs(c.Context, s, logOpts) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			// defer after err check as any err returns a nil io.ReadCloser | ||||||
|  | 			defer logs.Close() | ||||||
|  |  | ||||||
|  | 			_, err = io.Copy(os.Stdout, logs) | ||||||
|  | 			if err != nil && err != io.EOF { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 		}(service.ID) | ||||||
|  | 	} | ||||||
|  | 	wg.Wait() | ||||||
|  | 	os.Exit(0) | ||||||
|  | } | ||||||
| @ -27,8 +27,12 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe { | |||||||
|  |  | ||||||
| 	recipe, err := recipe.Get(recipeName) | 	recipe, err := recipe.Get(recipeName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | 		if c.Command.Name == "generate" { | ||||||
|  | 			logrus.Warn(err) | ||||||
|  | 		} else { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("validated '%s' as recipe argument", recipeName) | 	logrus.Debugf("validated '%s' as recipe argument", recipeName) | ||||||
|  |  | ||||||
| @ -108,6 +112,35 @@ func ValidateApp(c *cli.Context) config.App { | |||||||
| 	return app | 	return app | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ValidateAppByName ensures the app is valid and takes an app name as an argument, not context. | ||||||
|  | func ValidateAppByName(c *cli.Context, appName string) config.App { | ||||||
|  | 	if AppName != "" { | ||||||
|  | 		appName = AppName | ||||||
|  | 		logrus.Debugf("programmatically setting app name to %s", appName) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if appName == "" { | ||||||
|  | 		ShowSubcommandHelpAndError(c, errors.New("no app provided")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	app, err := app.Get(appName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := recipe.EnsureExists(app.Type); err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logrus.Debugf("validated '%s' as app argument", appName) | ||||||
|  |  | ||||||
|  | 	return app | ||||||
|  | } | ||||||
|  |  | ||||||
| // ValidateDomain ensures the domain name arg is valid. | // ValidateDomain ensures the domain name arg is valid. | ||||||
| func ValidateDomain(c *cli.Context) (string, error) { | func ValidateDomain(c *cli.Context) (string, error) { | ||||||
| 	domainName := c.Args().First() | 	domainName := c.Args().First() | ||||||
|  | |||||||
| @ -236,7 +236,7 @@ func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, d | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Infof("docker is installed on %s", domainName) | 	logrus.Infof("docker is already installed on %s", domainName) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @ -276,7 +276,8 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error | |||||||
| 		AdvertiseAddr: ipv4, | 		AdvertiseAddr: ipv4, | ||||||
| 	} | 	} | ||||||
| 	if _, err := cl.SwarmInit(c.Context, initReq); err != nil { | 	if _, err := cl.SwarmInit(c.Context, initReq); err != nil { | ||||||
| 		if !strings.Contains(err.Error(), "is already part of a swarm") { | 		if !strings.Contains(err.Error(), "is already part of a swarm") || | ||||||
|  | 			!strings.Contains(err.Error(), "must specify a listening address") { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		logrus.Infof("swarm mode already initialised on %s", domainName) | 		logrus.Infof("swarm mode already initialised on %s", domainName) | ||||||
|  | |||||||
							
								
								
									
										101
									
								
								cli/server/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								cli/server/logs.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | |||||||
|  | package server | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
|  | 	"coopcloud.tech/abra/cli/internal" | ||||||
|  | 	"coopcloud.tech/abra/pkg/client" | ||||||
|  | 	"coopcloud.tech/abra/pkg/config" | ||||||
|  | 	"github.com/docker/docker/api/types" | ||||||
|  | 	"github.com/docker/docker/api/types/filters" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/urfave/cli/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var Taillen string | ||||||
|  | var TaillenFlag = &cli.StringFlag{ | ||||||
|  | 	Name:        "tail", | ||||||
|  | 	Aliases:     []string{"t"}, | ||||||
|  | 	Value:       "5", | ||||||
|  | 	Destination: &Taillen, | ||||||
|  | 	Usage:       "change how many lines are shown", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var serverLogsCommand = &cli.Command{ | ||||||
|  | 	Name:      "logs", | ||||||
|  | 	Aliases:   []string{"l"}, | ||||||
|  | 	ArgsUsage: "<server>", | ||||||
|  | 	Usage:     "show logs from all apps from server", | ||||||
|  | 	Flags: []cli.Flag{ | ||||||
|  | 		TaillenFlag, | ||||||
|  | 		internal.StderrFlag, | ||||||
|  | 		internal.StdoutFlag, | ||||||
|  | 		internal.HealthcheckFlag, | ||||||
|  | 	}, | ||||||
|  | 	Action: func(c *cli.Context) error { | ||||||
|  | 		serverName, err := internal.ValidateServer(c) | ||||||
|  | 		serviceName := "" | ||||||
|  | 		if !internal.Stderr && !internal.Stdout && !internal.Healthcheck { | ||||||
|  | 			internal.Stderr = true | ||||||
|  | 			internal.Stdout = true | ||||||
|  | 			internal.Healthcheck = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		logrus.Debugf("flags parsed. --stderr: %t, --stdout: %t, --healthcheck: %t", internal.Stderr, internal.Stdout, internal.Healthcheck) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		appMap, err := config.LoadAppFiles(serverName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		logOpts := types.ContainerLogsOptions{ | ||||||
|  | 			Details:    false, | ||||||
|  | 			Follow:     false, | ||||||
|  | 			ShowStderr: internal.Stderr, | ||||||
|  | 			ShowStdout: internal.Stdout, | ||||||
|  | 			Tail:       Taillen, | ||||||
|  | 			Timestamps: true, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var appFiles []config.App | ||||||
|  | 		for appname, _ := range appMap { | ||||||
|  | 			app := internal.ValidateAppByName(c, appname) | ||||||
|  | 			appFiles = append(appFiles, app) | ||||||
|  | 		} | ||||||
|  | 		for _, app := range appFiles { | ||||||
|  | 			fmt.Println(app) | ||||||
|  | 			logrus.Debugf("checking logs for: %s", app.Name) | ||||||
|  | 			cl, err := client.New(app.Server) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			logrus.Debugf("tailing logs for all services") | ||||||
|  | 			filters := filters.NewArgs() | ||||||
|  | 			filters.Add("name", service) | ||||||
|  | 			serviceOpts := types.ServiceListOptions{Filters: filters} | ||||||
|  | 			services, err := cl.ServiceList(c.Context, serviceOpts) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			logrus.Info(app.StackName()) | ||||||
|  | 			for { | ||||||
|  | 				_, err = io.Copy(os.Stdout, logs) | ||||||
|  | 				if err == io.EOF { | ||||||
|  | 					break | ||||||
|  | 				} else if err != nil { | ||||||
|  | 					logrus.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			logs.Close() | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}, | ||||||
|  | } | ||||||
| @ -23,5 +23,6 @@ apps, see available flags on "server add" for more. | |||||||
| 		serverAddCommand, | 		serverAddCommand, | ||||||
| 		serverListCommand, | 		serverListCommand, | ||||||
| 		serverRemoveCommand, | 		serverRemoveCommand, | ||||||
|  | 		serverLogsCommand, | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -34,6 +34,7 @@ require ( | |||||||
| 	github.com/gliderlabs/ssh v0.3.3 | 	github.com/gliderlabs/ssh v0.3.3 | ||||||
| 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | ||||||
| 	github.com/gorilla/mux v1.8.0 // indirect | 	github.com/gorilla/mux v1.8.0 // indirect | ||||||
|  | 	github.com/hashicorp/go-retryablehttp v0.7.0 | ||||||
| 	github.com/kevinburke/ssh_config v1.1.0 | 	github.com/kevinburke/ssh_config v1.1.0 | ||||||
| 	github.com/libdns/gandi v1.0.2 | 	github.com/libdns/gandi v1.0.2 | ||||||
| 	github.com/libdns/libdns v0.2.1 | 	github.com/libdns/libdns v0.2.1 | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
								
							| @ -444,8 +444,14 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc | |||||||
| github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= | ||||||
| github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||||
|  | github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= | ||||||
|  | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||||
|  | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= | ||||||
|  | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= | ||||||
| github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= | github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= | ||||||
| github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= | ||||||
|  | github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= | ||||||
|  | github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= | ||||||
| github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
| github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strings" | 	"strings" | ||||||
| @ -46,6 +45,7 @@ type features struct { | |||||||
| 	Image       image  `json:"image"` | 	Image       image  `json:"image"` | ||||||
| 	Status      int    `json:"status"` | 	Status      int    `json:"status"` | ||||||
| 	Tests       string `json:"tests"` | 	Tests       string `json:"tests"` | ||||||
|  | 	SSO         string `json:"sso"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // tag represents a git tag. | // tag represents a git tag. | ||||||
| @ -122,7 +122,7 @@ func (r ByRecipeName) Less(i, j int) bool { | |||||||
| // recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally | // recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally | ||||||
| // is up to date. | // is up to date. | ||||||
| func recipeCatalogueFSIsLatest() (bool, error) { | func recipeCatalogueFSIsLatest() (bool, error) { | ||||||
| 	httpClient := &http.Client{Timeout: web.Timeout} | 	httpClient := web.NewHTTPRetryClient() | ||||||
| 	res, err := httpClient.Head(RecipeCatalogueURL) | 	res, err := httpClient.Head(RecipeCatalogueURL) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| @ -374,6 +374,127 @@ func ReadReposMetadata() (RepoCatalogue, error) { | |||||||
| 	return reposMeta, nil | 	return reposMeta, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func GetStringInBetween(str, start, end string) (result string, err error) { | ||||||
|  | 	// GetStringInBetween returns empty string if no start or end string found | ||||||
|  | 	s := strings.Index(str, start) | ||||||
|  | 	if s == -1 { | ||||||
|  | 		return "", fmt.Errorf("marker string '%s' not found", start) | ||||||
|  | 	} | ||||||
|  | 	s += len(start) | ||||||
|  | 	e := strings.Index(str[s:], end) | ||||||
|  | 	if e == -1 { | ||||||
|  | 		return "", fmt.Errorf("end marker '%s' not found", end) | ||||||
|  | 	} | ||||||
|  | 	return str[s : s+e], nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetImageMetadata(imageRowString string) (image, error) { | ||||||
|  | 	img := image{} | ||||||
|  |  | ||||||
|  | 	imgFields := strings.Split(imageRowString, ",") | ||||||
|  |  | ||||||
|  | 	for i, elem := range imgFields { | ||||||
|  | 		imgFields[i] = strings.TrimSpace(elem) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(imgFields) < 3 { | ||||||
|  | 		logrus.Warnf("image string has incorrect format: %s", imageRowString) | ||||||
|  | 		return img, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	img.Rating = imgFields[1] | ||||||
|  | 	img.Source = imgFields[2] | ||||||
|  |  | ||||||
|  | 	imgString := imgFields[0] | ||||||
|  |  | ||||||
|  | 	imageName, err := GetStringInBetween(imgString, "[", "]") | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	img.Image = strings.ReplaceAll(imageName, "`", "") | ||||||
|  |  | ||||||
|  | 	imageURL, err := GetStringInBetween(imgString, "(", ")") | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	img.URL = imageURL | ||||||
|  |  | ||||||
|  | 	return img, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetRecipeFeaturesAndCategory(recipeName string) (features, string, error) { | ||||||
|  | 	feat := features{} | ||||||
|  |  | ||||||
|  | 	var category string | ||||||
|  |  | ||||||
|  | 	readmePath := path.Join(config.ABRA_DIR, "apps", recipeName, "README.md") | ||||||
|  |  | ||||||
|  | 	logrus.Debugf("attempting to open '%s'", readmePath) | ||||||
|  |  | ||||||
|  | 	readmeFS, err := ioutil.ReadFile(readmePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return feat, category, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	readmeMetadata, err := GetStringInBetween( // Find text between delimiters | ||||||
|  | 		string(readmeFS), | ||||||
|  | 		"<!-- metadata -->", "<!-- endmetadata -->", | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return feat, category, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	readmeLines := strings.Split( // Array item from lines | ||||||
|  | 		strings.ReplaceAll( // Remove \t tabs | ||||||
|  | 			readmeMetadata, "\t", "", | ||||||
|  | 		), | ||||||
|  | 		"\n") | ||||||
|  |  | ||||||
|  | 	for _, val := range readmeLines { | ||||||
|  | 		if strings.Contains(val, "**Category**") { | ||||||
|  | 			category = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Category**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**Backups**") { | ||||||
|  | 			feat.Backups = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Backups**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**Email**") { | ||||||
|  | 			feat.Email = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Email**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**SSO**") { | ||||||
|  | 			feat.SSO = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **SSO**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**Healthcheck**") { | ||||||
|  | 			feat.Healthcheck = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Healthcheck**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**Tests**") { | ||||||
|  | 			feat.Tests = strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Tests**:"), | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(val, "**Image**") { | ||||||
|  | 			imageMetadata, err := GetImageMetadata(strings.TrimSpace( | ||||||
|  | 				strings.TrimPrefix(val, "* **Image**:"), | ||||||
|  | 			)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			feat.Image = imageMetadata | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return feat, category, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetRecipeVersions retrieves all recipe versions. | // GetRecipeVersions retrieves all recipe versions. | ||||||
| func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | ||||||
| 	versions := RecipeVersions{} | 	versions := RecipeVersions{} | ||||||
| @ -404,7 +525,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | |||||||
|  |  | ||||||
| 		checkOutOpts := &git.CheckoutOptions{ | 		checkOutOpts := &git.CheckoutOptions{ | ||||||
| 			Create: false, | 			Create: false, | ||||||
| 			Force:  true, | 			Keep:   true, | ||||||
| 			Branch: plumbing.ReferenceName(ref.Name()), | 			Branch: plumbing.ReferenceName(ref.Name()), | ||||||
| 		} | 		} | ||||||
| 		if err := worktree.Checkout(checkOutOpts); err != nil { | 		if err := worktree.Checkout(checkOutOpts); err != nil { | ||||||
| @ -432,9 +553,21 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | |||||||
| 				path = strings.Split(path, "/")[1] | 				path = strings.Split(path, "/")[1] | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			var tag string | ||||||
|  | 			switch img.(type) { | ||||||
|  | 			case reference.NamedTagged: | ||||||
|  | 				tag = img.(reference.NamedTagged).Tag() | ||||||
|  | 			case reference.Named: | ||||||
|  | 				logrus.Warnf("%s service is missing image tag?", path) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			logrus.Debugf("looking up image: '%s' from '%s'", img, path) | ||||||
|  |  | ||||||
| 			digest, err := client.GetTagDigest(img) | 			digest, err := client.GetTagDigest(img) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				logrus.Warn(err) | ||||||
|  | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			versionMeta[service.Name] = ServiceMeta{ | 			versionMeta[service.Name] = ServiceMeta{ | ||||||
| @ -465,7 +598,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | |||||||
| 	refName := fmt.Sprintf("refs/heads/%s", branch) | 	refName := fmt.Sprintf("refs/heads/%s", branch) | ||||||
| 	checkOutOpts := &git.CheckoutOptions{ | 	checkOutOpts := &git.CheckoutOptions{ | ||||||
| 		Create: false, | 		Create: false, | ||||||
| 		Force:  true, | 		Keep:   true, | ||||||
| 		Branch: plumbing.ReferenceName(refName), | 		Branch: plumbing.ReferenceName(refName), | ||||||
| 	} | 	} | ||||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||||
| @ -480,14 +613,9 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue. | // GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue. | ||||||
| func GetRecipeCatalogueVersions(recipeName string) ([]string, error) { | func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) { | ||||||
| 	var versions []string | 	var versions []string | ||||||
|  |  | ||||||
| 	catl, err := ReadRecipeCatalogue() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return versions, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if recipeMeta, exists := catl[recipeName]; exists { | 	if recipeMeta, exists := catl[recipeName]; exists { | ||||||
| 		for _, versionMeta := range recipeMeta.Versions { | 		for _, versionMeta := range recipeMeta.Versions { | ||||||
| 			for tag := range versionMeta { | 			for tag := range versionMeta { | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import ( | |||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/web" | 	"coopcloud.tech/abra/pkg/web" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/docker/distribution/reference" | ||||||
|  | 	"github.com/hashicorp/go-retryablehttp" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type RawTag struct { | type RawTag struct { | ||||||
| @ -35,12 +36,12 @@ func GetRegistryTags(image string) (RawTags, error) { | |||||||
| func getRegv2Token(image reference.Named) (string, error) { | func getRegv2Token(image reference.Named) (string, error) { | ||||||
| 	img := reference.Path(image) | 	img := reference.Path(image) | ||||||
| 	authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img) | 	authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img) | ||||||
| 	req, err := http.NewRequest("GET", authTokenURL, nil) | 	req, err := retryablehttp.NewRequest("GET", authTokenURL, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	client := &http.Client{Timeout: web.Timeout} | 	client := web.NewHTTPRetryClient() | ||||||
| 	res, err := client.Do(req) | 	res, err := client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| @ -78,7 +79,7 @@ func GetTagDigest(image reference.Named) (string, error) { | |||||||
| 	tag := image.(reference.NamedTagged).Tag() | 	tag := image.(reference.NamedTagged).Tag() | ||||||
| 	manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag) | 	manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag) | ||||||
|  |  | ||||||
| 	req, err := http.NewRequest("GET", manifestURL, nil) | 	req, err := retryablehttp.NewRequest("GET", manifestURL, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| @ -96,7 +97,7 @@ func GetTagDigest(image reference.Named) (string, error) { | |||||||
| 		"Authorization": []string{fmt.Sprintf("Bearer %s", token)}, | 		"Authorization": []string{fmt.Sprintf("Bearer %s", token)}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	client := &http.Client{Timeout: web.Timeout} | 	client := web.NewHTTPRetryClient() | ||||||
| 	res, err := client.Do(req) | 	res, err := client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
|  | |||||||
| @ -47,6 +47,16 @@ func EnsureUpToDate(dir string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	recipeName := filepath.Base(dir) | ||||||
|  | 	isClean, err := IsClean(recipeName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !isClean { | ||||||
|  | 		return fmt.Errorf("'%s' has locally unstaged changes", recipeName) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	branch := "master" | 	branch := "master" | ||||||
| 	if _, err := repo.Branch("master"); err != nil { | 	if _, err := repo.Branch("master"); err != nil { | ||||||
| 		if _, err := repo.Branch("main"); err != nil { | 		if _, err := repo.Branch("main"); err != nil { | ||||||
| @ -66,7 +76,7 @@ func EnsureUpToDate(dir string) error { | |||||||
| 	refName := fmt.Sprintf("refs/heads/%s", branch) | 	refName := fmt.Sprintf("refs/heads/%s", branch) | ||||||
| 	checkOutOpts := &git.CheckoutOptions{ | 	checkOutOpts := &git.CheckoutOptions{ | ||||||
| 		Create: false, | 		Create: false, | ||||||
| 		Force:  true, | 		Keep:   true, | ||||||
| 		Branch: plumbing.ReferenceName(refName), | 		Branch: plumbing.ReferenceName(refName), | ||||||
| 	} | 	} | ||||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||||
|  | |||||||
							
								
								
									
										104
									
								
								pkg/git/read.go
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								pkg/git/read.go
									
									
									
									
									
								
							| @ -1,11 +1,17 @@ | |||||||
| package git | package git | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os/user" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
|  | 	gitConfigPkg "github.com/go-git/go-git/v5/config" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"github.com/go-git/go-git/v5/plumbing" | ||||||
|  | 	"github.com/go-git/go-git/v5/plumbing/format/gitignore" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -40,6 +46,12 @@ func IsClean(recipeName string) (bool, error) { | |||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	patterns, err := GetExcludesFiles() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	worktree.Excludes = append(patterns, worktree.Excludes...) | ||||||
|  |  | ||||||
| 	status, err := worktree.Status() | 	status, err := worktree.Status() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| @ -53,3 +65,95 @@ func IsClean(recipeName string) (bool, error) { | |||||||
|  |  | ||||||
| 	return status.IsClean(), nil | 	return status.IsClean(), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetExcludesFiles reads the exlude files from a global git ignore | ||||||
|  | func GetExcludesFiles() ([]gitignore.Pattern, error) { | ||||||
|  | 	var err error | ||||||
|  | 	var patterns []gitignore.Pattern | ||||||
|  |  | ||||||
|  | 	cfg, err := parseGitConfig() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return patterns, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	excludesfile := getExcludesFile(cfg) | ||||||
|  | 	patterns, err = parseExcludesFile(excludesfile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return patterns, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return patterns, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseGitConfig() (*gitConfigPkg.Config, error) { | ||||||
|  | 	cfg := gitConfigPkg.NewConfig() | ||||||
|  |  | ||||||
|  | 	usr, err := user.Current() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b, err := ioutil.ReadFile(usr.HomeDir + "/.gitconfig") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := cfg.Unmarshal(b); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return cfg, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getExcludesFile(cfg *gitConfigPkg.Config) string { | ||||||
|  | 	for _, sec := range cfg.Raw.Sections { | ||||||
|  | 		if sec.Name == "core" { | ||||||
|  | 			for _, opt := range sec.Options { | ||||||
|  | 				if opt.Key == "excludesfile" { | ||||||
|  | 					return opt.Value | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) { | ||||||
|  | 	excludesfile, err := expandTilde(excludesfile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := ioutil.ReadFile(excludesfile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ps []gitignore.Pattern | ||||||
|  | 	for _, s := range strings.Split(string(data), "\n") { | ||||||
|  | 		if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 { | ||||||
|  | 			ps = append(ps, gitignore.ParsePattern(s, nil)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ps, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func expandTilde(path string) (string, error) { | ||||||
|  | 	if !strings.HasPrefix(path, "~") { | ||||||
|  | 		return path, nil | ||||||
|  | 	} | ||||||
|  | 	var paths []string | ||||||
|  | 	u, err := user.Current() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	for _, p := range strings.Split(path, string(filepath.Separator)) { | ||||||
|  | 		if p == "~" { | ||||||
|  | 			paths = append(paths, u.HomeDir) | ||||||
|  | 		} else { | ||||||
|  | 			paths = append(paths, p) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "/" + filepath.Join(paths...), nil | ||||||
|  | } | ||||||
|  | |||||||
| @ -84,7 +84,7 @@ func Get(recipeName string) (Recipe, error) { | |||||||
| 	envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") | 	envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") | ||||||
| 	sampleEnv, err := config.ReadEnv(envSamplePath) | 	sampleEnv, err := config.ReadEnv(envSamplePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logrus.Fatal(err) | 		return Recipe{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts := stack.Deploy{Composefiles: composeFiles} | 	opts := stack.Deploy{Composefiles: composeFiles} | ||||||
| @ -157,7 +157,8 @@ func EnsureVersion(recipeName, version string) error { | |||||||
| 	logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName) | 	logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName) | ||||||
|  |  | ||||||
| 	if tagRef.String() == "" { | 	if tagRef.String() == "" { | ||||||
| 		return fmt.Errorf("%s is not available?", version) | 		logrus.Warnf("%s recipe has no local tag: %s? this recipe version is not released?", recipeName, version) | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	worktree, err := repo.Worktree() | 	worktree, err := repo.Worktree() | ||||||
| @ -168,7 +169,7 @@ func EnsureVersion(recipeName, version string) error { | |||||||
| 	opts := &git.CheckoutOptions{ | 	opts := &git.CheckoutOptions{ | ||||||
| 		Branch: tagRef, | 		Branch: tagRef, | ||||||
| 		Create: false, | 		Create: false, | ||||||
| 		Force:  true, | 		Keep:   true, | ||||||
| 	} | 	} | ||||||
| 	if err := worktree.Checkout(opts); err != nil { | 	if err := worktree.Checkout(opts); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -220,7 +221,7 @@ func EnsureLatest(recipeName string) error { | |||||||
| 	refName := fmt.Sprintf("refs/heads/%s", branch) | 	refName := fmt.Sprintf("refs/heads/%s", branch) | ||||||
| 	checkOutOpts := &git.CheckoutOptions{ | 	checkOutOpts := &git.CheckoutOptions{ | ||||||
| 		Create: false, | 		Create: false, | ||||||
| 		Force:  true, | 		Keep:   true, | ||||||
| 		Branch: plumbing.ReferenceName(refName), | 		Branch: plumbing.ReferenceName(refName), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | |||||||
| @ -330,9 +330,9 @@ func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.Publ | |||||||
|  |  | ||||||
| 		fmt.Printf(fmt.Sprintf(` | 		fmt.Printf(fmt.Sprintf(` | ||||||
| You are attempting to make an SSH connection to a server but there is no entry | You are attempting to make an SSH connection to a server but there is no entry | ||||||
| in your ~/.ssh/known_hosts file which confirms that this is indeed the server | in your ~/.ssh/known_hosts file which confirms that you have already validated | ||||||
| you want to connect to. Please take a moment to validate the following SSH host | that this is indeed the server you want to connect to. Please take a moment to | ||||||
| key, it is important. | validate the following SSH host key, it is important. | ||||||
|  |  | ||||||
|     Host:        %s |     Host:        %s | ||||||
|     Fingerprint: %s |     Fingerprint: %s | ||||||
|  | |||||||
| @ -13,6 +13,11 @@ import ( | |||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // DontSkipValidation ensures validation is done for compose file loading | ||||||
|  | func DontSkipValidation(opts *loader.Options) { | ||||||
|  | 	opts.SkipValidation = false | ||||||
|  | } | ||||||
|  |  | ||||||
| // LoadComposefile parse the composefile specified in the cli and returns its Config and version. | // LoadComposefile parse the composefile specified in the cli and returns its Config and version. | ||||||
| func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) { | func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) { | ||||||
| 	configDetails, err := getConfigDetails(opts.Composefiles, appEnv) | 	configDetails, err := getConfigDetails(opts.Composefiles, appEnv) | ||||||
| @ -21,13 +26,12 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	dicts := getDictsFrom(configDetails.ConfigFiles) | 	dicts := getDictsFrom(configDetails.ConfigFiles) | ||||||
| 	config, err := loader.Load(configDetails) | 	config, err := loader.Load(configDetails, DontSkipValidation) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { | 		if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { | ||||||
| 			return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s", | 			return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s", | ||||||
| 				propertyWarnings(fpe.Properties)) | 				propertyWarnings(fpe.Properties)) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | |||||||
| @ -350,7 +350,7 @@ func deployServices( | |||||||
| 		existingServiceMap[service.Spec.Name] = service | 		existingServiceMap[service.Spec.Name] = service | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var serviceIDs []string | 	serviceIDs := make(map[string]string) | ||||||
| 	for internalName, serviceSpec := range services { | 	for internalName, serviceSpec := range services { | ||||||
| 		var ( | 		var ( | ||||||
| 			name        = namespace.Scope(internalName) | 			name        = namespace.Scope(internalName) | ||||||
| @ -410,7 +410,7 @@ func deployServices( | |||||||
| 				return errors.Wrapf(err, "failed to update service %s", name) | 				return errors.Wrapf(err, "failed to update service %s", name) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			serviceIDs = append(serviceIDs, service.ID) | 			serviceIDs[service.ID] = name | ||||||
|  |  | ||||||
| 			for _, warning := range response.Warnings { | 			for _, warning := range response.Warnings { | ||||||
| 				logrus.Warn(warning) | 				logrus.Warn(warning) | ||||||
| @ -430,15 +430,19 @@ func deployServices( | |||||||
| 				return errors.Wrapf(err, "failed to create service %s", name) | 				return errors.Wrapf(err, "failed to create service %s", name) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			serviceIDs = append(serviceIDs, serviceCreateResponse.ID) | 			serviceIDs[serviceCreateResponse.ID] = name | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Infof("waiting for services to converge: %s", strings.Join(serviceIDs, ", ")) | 	var serviceNames []string | ||||||
|  | 	for _, serviceName := range serviceIDs { | ||||||
|  | 		serviceNames = append(serviceNames, serviceName) | ||||||
|  | 	} | ||||||
|  | 	logrus.Infof("waiting for services to converge: %s", strings.Join(serviceNames, ", ")) | ||||||
|  |  | ||||||
| 	ch := make(chan error, len(serviceIDs)) | 	ch := make(chan error, len(serviceIDs)) | ||||||
| 	for _, serviceID := range serviceIDs { | 	for serviceID, serviceName := range serviceIDs { | ||||||
| 		logrus.Debugf("waiting on %s to converge", serviceID) | 		logrus.Debugf("waiting on %s to converge", serviceName) | ||||||
| 		go func(s string) { | 		go func(s string) { | ||||||
| 			ch <- waitOnService(ctx, cl, s) | 			ch <- waitOnService(ctx, cl, s) | ||||||
| 		}(serviceID) | 		}(serviceID) | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								pkg/web/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								pkg/web/client.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | package web | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/hashicorp/go-retryablehttp" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type customLeveledLogger struct { | ||||||
|  | 	retryablehttp.Logger | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l customLeveledLogger) Printf(msg string, args ...interface{}) { | ||||||
|  | 	logrus.Debugf(fmt.Sprintf(msg, args...)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewHTTPRetryClient instantiates a new http client with retries baked in | ||||||
|  | func NewHTTPRetryClient() *retryablehttp.Client { | ||||||
|  | 	retryClient := retryablehttp.NewClient() | ||||||
|  | 	retryClient.Logger = customLeveledLogger{} | ||||||
|  | 	return retryClient | ||||||
|  | } | ||||||
| @ -3,7 +3,6 @@ package web | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -13,7 +12,7 @@ const Timeout = 10 * time.Second | |||||||
|  |  | ||||||
| // ReadJSON reads JSON and parses it into your chosen interface pointer | // ReadJSON reads JSON and parses it into your chosen interface pointer | ||||||
| func ReadJSON(url string, target interface{}) error { | func ReadJSON(url string, target interface{}) error { | ||||||
| 	httpClient := &http.Client{Timeout: Timeout} | 	httpClient := NewHTTPRetryClient() | ||||||
| 	res, err := httpClient.Get(url) | 	res, err := httpClient.Get(url) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | |||||||
| @ -76,13 +76,13 @@ function install_abra_release { | |||||||
|       p=$HOME/.local/bin |       p=$HOME/.local/bin | ||||||
|       com="echo PATH=\$PATH:$p" |       com="echo PATH=\$PATH:$p" | ||||||
|       if [[ $SHELL =~ "bash" ]]; then |       if [[ $SHELL =~ "bash" ]]; then | ||||||
|           echo "echo $com >> $HOME/.bashrc" |           echo "$com >> $HOME/.bashrc" | ||||||
|       elif [[ $SHELL =~ "fizsh" ]]; then |       elif [[ $SHELL =~ "fizsh" ]]; then | ||||||
|           echo "echo $com >> $HOME/.fizsh/.fizshrc" |           echo "$com >> $HOME/.fizsh/.fizshrc" | ||||||
|       elif [[ $SHELL =~ "zsh" ]]; then |       elif [[ $SHELL =~ "zsh" ]]; then | ||||||
|           echo "echo $com >> $HOME/.zshrc" |           echo "$com >> $HOME/.zshrc" | ||||||
|       else |       else | ||||||
|           echo "echo $com >> $HOME/.profile" |           echo "$com >> $HOME/.profile" | ||||||
|       fi |       fi | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user