forked from toolshed/abra
		
	Compare commits
	
		
			45 Commits
		
	
	
		
			0.2.0-alph
			...
			0.2.2-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8b15f2de5b | |||
| cdb76e7276 | |||
| a170e26e27 | |||
| 03b1882b81 | |||
| 2fcdaca75f | |||
| c5f44cf340 | |||
| 7a5ad65178 | |||
| 6d4ee3de0d | |||
| 63318fb6ff | |||
| 07ffa08a07 | |||
| 0e5e7490b3 | |||
| 640032b8fe | |||
| 39babea963 | |||
| 07613f5163 | |||
| 7f1d9eeaec | |||
| 02d24104e1 | |||
| da8d72620a | |||
| 96ccadc70f | |||
| 8703370785 | |||
| 7d8c53299d | |||
| 0110aceb1f | |||
| aec1e4520d | |||
| 74bcb99c70 | |||
| dd4f2b48ec | |||
| 7f3f41ede4 | |||
| 597b4b586e | |||
| 7ea3df45d4 | |||
| 5941ed9728 | |||
| d1e42752e2 | |||
| 9dfbd21c61 | |||
| 9526d1fde6 | |||
| 62cc7ef92d | |||
| c5a7a831d2 | |||
| 4aae186f5f | |||
| 2f9b11f389 | |||
| 6d42e72f16 | |||
| 5be190e110 | |||
| c1390f232e | |||
| 95e19f03c4 | |||
| dc040a0b38 | |||
| e6e2e5214f | |||
| 61452b5f32 | |||
| 78460ac0ba | |||
| 0615c3f745 | |||
| e820e0219d | 
| @ -1,40 +0,0 @@ | ||||
| {{ range .Versions }} | ||||
| <a name="{{ .Tag.Name }}"></a> | ||||
| ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} | ||||
|  | ||||
| > {{ datetime "2006-01-02" .Tag.Date }} | ||||
|  | ||||
| {{ range .CommitGroups -}} | ||||
| ### {{ .Title }} | ||||
|  | ||||
| {{ range .Commits -}} | ||||
| * {{ .Subject }} | ||||
| {{ end }} | ||||
| {{ end -}} | ||||
|  | ||||
| {{- if .RevertCommits -}} | ||||
| ### Reverts | ||||
|  | ||||
| {{ range .RevertCommits -}} | ||||
| * {{ .Revert.Header }} | ||||
| {{ end }} | ||||
| {{ end -}} | ||||
|  | ||||
| {{- if .MergeCommits -}} | ||||
| ### Pull Requests | ||||
|  | ||||
| {{ range .MergeCommits -}} | ||||
| * {{ .Header }} | ||||
| {{ end }} | ||||
| {{ end -}} | ||||
|  | ||||
| {{- if .NoteGroups -}} | ||||
| {{ range .NoteGroups -}} | ||||
| ### {{ .Title }} | ||||
|  | ||||
| {{ range .Notes }} | ||||
| {{ .Body }} | ||||
| {{ end }} | ||||
| {{ end -}} | ||||
| {{ end -}} | ||||
| {{ end -}} | ||||
| @ -1,27 +0,0 @@ | ||||
| style: github | ||||
| template: CHANGELOG.tpl.md | ||||
| info: | ||||
|   title: CHANGELOG | ||||
|   repository_url: https://git.autonomic.zone:2222/coop-cloud/go-abra | ||||
| options: | ||||
|   commits: | ||||
|     # filters: | ||||
|     #   Type: | ||||
|     #     - feat | ||||
|     #     - fix | ||||
|     #     - perf | ||||
|     #     - refactor | ||||
|   commit_groups: | ||||
|     # title_maps: | ||||
|     #   feat: Features | ||||
|     #   fix: Bug Fixes | ||||
|     #   perf: Performance Improvements | ||||
|     #   refactor: Code Refactoring | ||||
|   header: | ||||
|     pattern: "^(\\w*)\\:\\s(.*)$" | ||||
|     pattern_maps: | ||||
|       - Type | ||||
|       - Subject | ||||
|   notes: | ||||
|     keywords: | ||||
|       - BREAKING CHANGE | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -3,3 +3,4 @@ abra | ||||
| vendor/ | ||||
| .envrc | ||||
| dist/ | ||||
| *fmtcoverage.html | ||||
|  | ||||
| @ -50,6 +50,13 @@ sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra | ||||
| source /etc/bash_completion.d/abra | ||||
| ``` | ||||
|  | ||||
| In development, you can source the script in your git checkout, just make sure | ||||
| to set `PROG=abra`, otherwise it'll add completion to the wrong command: | ||||
|  | ||||
| ``` | ||||
| PROG=abra source /path/to/abra/scripts/autocomplete/bash | ||||
| ``` | ||||
|  | ||||
| **(fi)zsh** | ||||
|  | ||||
| (fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc` | ||||
|  | ||||
| @ -7,7 +7,7 @@ import ( | ||||
| // AppCommand defines the `abra app` command and ets subcommands | ||||
| var AppCommand = &cli.Command{ | ||||
| 	Name:      "app", | ||||
| 	Usage:     "Manage apps", | ||||
| 	Usage:     "Manage deployed apps", | ||||
| 	Aliases:   []string{"a"}, | ||||
| 	ArgsUsage: "<app>", | ||||
| 	Description: ` | ||||
|  | ||||
| @ -22,6 +22,7 @@ var appDeployCommand = &cli.Command{ | ||||
| 	Usage:   "Deploy an app", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 		internal.ChaosFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command deploys a new instance of an app. It does not support changing the | ||||
| @ -29,8 +30,11 @@ version of an existing deployed app, for this you need to look at the "abra app | ||||
| upgrade <app>" command. | ||||
|  | ||||
| You may pass "--force" to re-deploy the same version again. This can be useful | ||||
| if the container runtime has gotten into a weird state or your doing some live | ||||
| hacking. | ||||
| if the container runtime has gotten into a weird state. | ||||
|  | ||||
| Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, | ||||
| including unstaged changes and can be useful for live hacking and testing new | ||||
| recipes. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| @ -50,33 +54,51 @@ hacking. | ||||
|  | ||||
| 		if isDeployed { | ||||
| 			if internal.Force { | ||||
| 				logrus.Infof("continuing with deployment due to '--force/-f' being set") | ||||
| 				logrus.Warnf("'%s' already deployed but continuing (--force)", stackName) | ||||
| 			} else if internal.Chaos { | ||||
| 				logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName) | ||||
| 			} else { | ||||
| 				logrus.Fatalf("'%s' is already deployed", stackName) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		version := deployedVersion | ||||
| 		if version == "" { | ||||
| 		if version == "" && !internal.Chaos { | ||||
| 			versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if len(versions) > 0 { | ||||
| 				version = versions[len(versions)-1] | ||||
| 				logrus.Infof("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) | ||||
| 				} | ||||
| 			} else { | ||||
| 				version = "latest commit" | ||||
| 				logrus.Warning("no versions detected, using latest commit") | ||||
| 				logrus.Warn("no versions detected, using latest commit") | ||||
| 				if err := recipe.EnsureLatest(app.Type); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if version == "" && !internal.Chaos { | ||||
| 			logrus.Debugf("choosing '%s' as version to deploy", version) | ||||
| 			if err := recipe.EnsureVersion(app.Type, version); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if internal.Chaos { | ||||
| 			logrus.Warnf("chaos mode engaged") | ||||
| 			var err error | ||||
| 			version, err = recipe.ChaosVersion(app.Type) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||
| 		if err != nil { | ||||
| @ -101,7 +123,7 @@ hacking. | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := DeployOverview(app, version); err != nil { | ||||
| 		if err := DeployOverview(app, version, "continue with deployment?"); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -126,7 +148,7 @@ hacking. | ||||
| } | ||||
|  | ||||
| // DeployOverview shows a deployment overview | ||||
| func DeployOverview(app config.App, version string) error { | ||||
| func DeployOverview(app config.App, version, message string) error { | ||||
| 	tableCol := []string{"server", "compose", "domain", "stack", "version"} | ||||
| 	table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| @ -135,12 +157,17 @@ func DeployOverview(app config.App, version string) error { | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version}) | ||||
| 	server := app.Server | ||||
| 	if app.Server == "default" { | ||||
| 		server = "local" | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{server, deployConfig, app.Domain, app.StackName(), version}) | ||||
| 	table.Render() | ||||
|  | ||||
| 	response := false | ||||
| 	prompt := &survey.Confirm{ | ||||
| 		Message: "continue with deployment?", | ||||
| 		Message: message, | ||||
| 	} | ||||
|  | ||||
| 	if err := survey.AskOne(prompt, &response); err != nil { | ||||
| @ -164,7 +191,12 @@ func NewVersionOverview(app config.App, currentVersion, newVersion string) error | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion}) | ||||
| 	server := app.Server | ||||
| 	if app.Server == "default" { | ||||
| 		server = "local" | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion}) | ||||
| 	table.Render() | ||||
|  | ||||
| 	response := false | ||||
|  | ||||
| @ -210,6 +210,10 @@ func action(c *cli.Context) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if newAppServer == "default" { | ||||
| 		newAppServer = "local" | ||||
| 	} | ||||
|  | ||||
| 	tableCol := []string{"Name", "Domain", "Type", "Server"} | ||||
| 	table := abraFormatter.CreateTable(tableCol) | ||||
| 	table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer}) | ||||
|  | ||||
| @ -23,16 +23,20 @@ var appRollbackCommand = &cli.Command{ | ||||
| 	ArgsUsage: "<app>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 		internal.ChaosFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command rolls an app back to a previous version if one exists. | ||||
|  | ||||
| You may pass "--force/-f" to downgrade to the same version again. This can be | ||||
| useful if the container runtime has gotten into a weird state or your doing | ||||
| some live hacking. | ||||
| useful if the container runtime has gotten into a weird state. | ||||
|  | ||||
| This action could be destructive, please ensure you have a copy of your app | ||||
| data beforehand - see "abra app backup <app>" for more. | ||||
|  | ||||
| Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, | ||||
| including unstaged changes and can be useful for live hacking and testing new | ||||
| recipes. | ||||
| `, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		appNames, err := config.GetAppNames() | ||||
| @ -62,10 +66,6 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if deployedVersion == "" { | ||||
| 			logrus.Fatalf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if !isDeployed { | ||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||
| 		} | ||||
| @ -76,22 +76,30 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 		} | ||||
|  | ||||
| 		var availableDowngrades []string | ||||
| 		for _, version := range versions { | ||||
| 			parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			parsedVersion, err := tagcmp.Parse(version) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) { | ||||
| 				availableDowngrades = append(availableDowngrades, version) | ||||
| 			} | ||||
| 		if deployedVersion == "" { | ||||
| 			deployedVersion = "unknown" | ||||
| 			availableDowngrades = versions | ||||
| 			logrus.Warnf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if len(availableDowngrades) == 0 { | ||||
| 			logrus.Fatal("no available downgrades, you're on latest") | ||||
| 		if deployedVersion != "unknown" && !internal.Chaos { | ||||
| 			for _, version := range versions { | ||||
| 				parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				parsedVersion, err := tagcmp.Parse(version) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) { | ||||
| 					availableDowngrades = append(availableDowngrades, version) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(availableDowngrades) == 0 { | ||||
| 				logrus.Fatal("no available downgrades, you're on latest") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// FIXME: jeezus golang why do you not have a list reverse function | ||||
| @ -100,23 +108,34 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 		} | ||||
|  | ||||
| 		var chosenDowngrade string | ||||
| 		if !internal.Force { | ||||
| 			prompt := &survey.Select{ | ||||
| 				Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion), | ||||
| 				Options: availableDowngrades, | ||||
| 			} | ||||
| 			if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { | ||||
| 				return err | ||||
| 		if !internal.Chaos { | ||||
| 			if internal.Force { | ||||
| 				chosenDowngrade = availableDowngrades[0] | ||||
| 				logrus.Debugf("choosing '%s' as version to downgrade to (--force)", chosenDowngrade) | ||||
| 			} else { | ||||
| 				prompt := &survey.Select{ | ||||
| 					Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion), | ||||
| 					Options: availableDowngrades, | ||||
| 				} | ||||
| 				if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if internal.Force { | ||||
| 			chosenDowngrade = availableDowngrades[0] | ||||
| 			logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade) | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		if internal.Chaos { | ||||
| 			logrus.Warn("chaos mode engaged") | ||||
| 			var err error | ||||
| 			chosenDowngrade, err = recipe.ChaosVersion(app.Type) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
|  | ||||
| @ -55,15 +55,20 @@ var appRunCommand = &cli.Command{ | ||||
| 		} | ||||
|  | ||||
| 		serviceName := c.Args().Get(1) | ||||
| 		stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName) | ||||
| 		filters := filters.NewArgs() | ||||
| 		filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName)) | ||||
| 		filters.Add("name", stackAndServiceName) | ||||
|  | ||||
| 		containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters}) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if len(containers) == 0 { | ||||
| 			logrus.Fatalf("no containers matching '%s' found?", stackAndServiceName) | ||||
| 		} | ||||
| 		if len(containers) > 1 { | ||||
| 			logrus.Fatalf("expected 1 container but got %d", len(containers)) | ||||
| 			logrus.Fatalf("expected 1 container matching '%s' but got %d", stackAndServiceName, len(containers)) | ||||
| 		} | ||||
|  | ||||
| 		cmd := c.Args().Slice()[2:] | ||||
|  | ||||
| @ -22,12 +22,28 @@ volumes as eligiblef or pruning once undeployed. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		cl, err := client.New(app.Server) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("checking whether '%s' is already deployed", stackName) | ||||
|  | ||||
| 		isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !isDeployed { | ||||
| 			logrus.Fatalf("'%s' is not deployed?", stackName) | ||||
| 		} | ||||
|  | ||||
| 		if err := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		rmOpts := stack.Remove{Namespaces: []string{app.StackName()}} | ||||
| 		if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
|  | ||||
| @ -22,6 +22,7 @@ var appUpgradeCommand = &cli.Command{ | ||||
| 	ArgsUsage: "<app>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 		internal.ChaosFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command supports upgrading an app. You can use it to choose and roll out a | ||||
| @ -32,11 +33,14 @@ opposed to "abra app deploy <app>" which will not change the version of a | ||||
| deployed app. | ||||
|  | ||||
| You may pass "--force/-f" to upgrade to the same version again. This can be | ||||
| useful if the container runtime has gotten into a weird state or your doing | ||||
| some live hacking. | ||||
| useful if the container runtime has gotten into a weird state. | ||||
|  | ||||
| This action could be destructive, please ensure you have a copy of your app | ||||
| data beforehand - see "abra app backup <app>" for more. | ||||
|  | ||||
| Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, | ||||
| including unstaged changes and can be useful for live hacking and testing new | ||||
| recipes. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| @ -54,10 +58,6 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if deployedVersion == "" { | ||||
| 			logrus.Fatalf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if !isDeployed { | ||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||
| 		} | ||||
| @ -67,43 +67,67 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		var availableUpgrades []string | ||||
| 		for _, version := range versions { | ||||
| 			parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			parsedVersion, err := tagcmp.Parse(version) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if parsedVersion.IsGreaterThan(parsedDeployedVersion) { | ||||
| 				availableUpgrades = append(availableUpgrades, version) | ||||
| 			} | ||||
| 		if len(versions) == 0 && !internal.Chaos { | ||||
| 			logrus.Fatalf("no versions available '%s' in recipe catalogue?", app.Type) | ||||
| 		} | ||||
|  | ||||
| 		if len(availableUpgrades) == 0 { | ||||
| 			logrus.Fatal("no available upgrades, you're on latest") | ||||
| 		var availableUpgrades []string | ||||
| 		if deployedVersion == "" { | ||||
| 			deployedVersion = "unknown" | ||||
| 			availableUpgrades = versions | ||||
| 			logrus.Warnf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if deployedVersion != "unknown" && !internal.Chaos { | ||||
| 			for _, version := range versions { | ||||
| 				parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				parsedVersion, err := tagcmp.Parse(version) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				if parsedVersion.IsGreaterThan(parsedDeployedVersion) { | ||||
| 					availableUpgrades = append(availableUpgrades, version) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(availableUpgrades) == 0 && !internal.Force { | ||||
| 				logrus.Fatal("no available upgrades, you're on latest") | ||||
| 				availableUpgrades = versions | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var chosenUpgrade string | ||||
| 		if !internal.Force { | ||||
| 			prompt := &survey.Select{ | ||||
| 				Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion), | ||||
| 				Options: availableUpgrades, | ||||
| 			} | ||||
| 			if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | ||||
| 				return err | ||||
| 		if len(availableUpgrades) > 0 && !internal.Chaos { | ||||
| 			if internal.Force { | ||||
| 				chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] | ||||
| 				logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade) | ||||
| 			} else { | ||||
| 				prompt := &survey.Select{ | ||||
| 					Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion), | ||||
| 					Options: availableUpgrades, | ||||
| 				} | ||||
| 				if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if internal.Force { | ||||
| 			chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] | ||||
| 			logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade) | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		if internal.Chaos { | ||||
| 			logrus.Warn("chaos mode engaged") | ||||
| 			var err error | ||||
| 			chosenUpgrade, err = recipe.ChaosVersion(app.Type) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
| @ -130,10 +154,8 @@ data beforehand - see "abra app backup <app>" for more. | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Force { | ||||
| 			if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { | ||||
|  | ||||
| @ -7,7 +7,7 @@ import ( | ||||
| // CatalogueCommand defines the `abra catalogue` command and sub-commands. | ||||
| var CatalogueCommand = &cli.Command{ | ||||
| 	Name:        "catalogue", | ||||
| 	Usage:       "Manage the recipe catalogue", | ||||
| 	Usage:       "Manage the recipe catalogue (for maintainers)", | ||||
| 	Aliases:     []string{"c"}, | ||||
| 	ArgsUsage:   "<recipe>", | ||||
| 	Description: "This command helps recipe packagers interact with the recipe catalogue", | ||||
|  | ||||
							
								
								
									
										12
									
								
								cli/cli.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								cli/cli.go
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/app" | ||||
| 	"coopcloud.tech/abra/cli/catalogue" | ||||
| 	"coopcloud.tech/abra/cli/domain" | ||||
| 	"coopcloud.tech/abra/cli/recipe" | ||||
| 	"coopcloud.tech/abra/cli/server" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| @ -40,8 +41,7 @@ var DebugFlag = &cli.BoolFlag{ | ||||
| 	Usage:       "Show DEBUG messages", | ||||
| } | ||||
|  | ||||
| // RunApp runs CLI abra app. | ||||
| func RunApp(version, commit string) { | ||||
| func newAbraApp(version, commit string) *cli.App { | ||||
| 	app := &cli.App{ | ||||
| 		Name: "abra", | ||||
| 		Usage: `The Co-op Cloud command-line utility belt 🎩🐇 | ||||
| @ -59,7 +59,7 @@ func RunApp(version, commit string) { | ||||
| 			server.ServerCommand, | ||||
| 			recipe.RecipeCommand, | ||||
| 			catalogue.CatalogueCommand, | ||||
| 			VersionCommand, | ||||
| 			domain.DomainCommand, | ||||
| 			UpgradeCommand, | ||||
| 		}, | ||||
| 		Flags: []cli.Flag{ | ||||
| @ -106,6 +106,12 @@ func RunApp(version, commit string) { | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
| 	return app | ||||
| } | ||||
|  | ||||
| // RunApp runs CLI abra app. | ||||
| func RunApp(version, commit string) { | ||||
| 	app := newAbraApp(version, commit) | ||||
|  | ||||
| 	if err := app.Run(os.Args); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
|  | ||||
							
								
								
									
										150
									
								
								cli/domain/create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								cli/domain/create.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| package domain | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	gandiPkg "coopcloud.tech/abra/pkg/dns/gandi" | ||||
| 	"github.com/libdns/gandi" | ||||
| 	"github.com/libdns/libdns" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // DomainCreateCommand lists domains. | ||||
| var DomainCreateCommand = &cli.Command{ | ||||
| 	Name:      "create", | ||||
| 	Usage:     "Create a new domain record", | ||||
| 	Aliases:   []string{"c"}, | ||||
| 	ArgsUsage: "<zone> <type> <name> <value> [<ttl>] [<priority>]", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.DNSProviderFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command creates a new domain name record for a specific zone. | ||||
|  | ||||
| You must specify a zone (e.g. example.com) under which your domain name records | ||||
| are listed. This zone must already be created on your provider account. | ||||
|  | ||||
| Example: | ||||
|  | ||||
|     abra domain create -p gandi foo.com A myapp 192.168.178.44 | ||||
|  | ||||
| Which means you can then deploy an app against "myapp.foo.com" successfully. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		zone := c.Args().First() | ||||
| 		if zone == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided")) | ||||
| 		} | ||||
|  | ||||
| 		recordType := c.Args().Get(1) | ||||
| 		recordName := c.Args().Get(2) | ||||
| 		recordValue := c.Args().Get(3) | ||||
| 		recordTTL := c.Args().Get(4) | ||||
| 		recordPriority := c.Args().Get(5) | ||||
|  | ||||
| 		if recordType == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no type provided")) | ||||
| 		} | ||||
| 		if recordName == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) | ||||
| 		} | ||||
| 		if recordValue == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no value provided")) | ||||
| 		} | ||||
|  | ||||
| 		if recordTTL == "" { | ||||
| 			recordTTL = "86400" | ||||
| 		} | ||||
|  | ||||
| 		if recordType == "MX" || recordType == "SRV" || recordType == "URI" { | ||||
| 			if recordPriority == "" { | ||||
| 				logrus.Fatal("record priority must be set when using MX/SRV/URI records") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		var provider gandi.Provider | ||||
| 		switch internal.DNSProvider { | ||||
| 		case "gandi": | ||||
| 			provider, err = gandiPkg.New() | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		default: | ||||
| 			logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider) | ||||
| 		} | ||||
|  | ||||
| 		ttl, err := strconv.Atoi(recordTTL) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		record := libdns.Record{ | ||||
| 			Type:  recordType, | ||||
| 			Name:  recordName, | ||||
| 			Value: recordValue, | ||||
| 			TTL:   time.Duration(ttl), | ||||
| 		} | ||||
|  | ||||
| 		if recordType == "MX" || recordType == "SRV" || recordType == "URI" { | ||||
| 			priority, err := strconv.Atoi(recordPriority) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			record.Priority = priority | ||||
| 		} | ||||
|  | ||||
| 		records, err := provider.GetRecords(c.Context, zone) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		for _, existingRecord := range records { | ||||
| 			if existingRecord.Type == record.Type && | ||||
| 				existingRecord.Name == record.Name && | ||||
| 				existingRecord.Value == record.Value { | ||||
| 				logrus.Fatal("provider library reports that this record already exists?") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		createdRecords, err := provider.SetRecords( | ||||
| 			c.Context, | ||||
| 			zone, | ||||
| 			[]libdns.Record{record}, | ||||
| 		) | ||||
|  | ||||
| 		if len(createdRecords) == 0 { | ||||
| 			logrus.Fatal("provider library reports no record created?") | ||||
| 		} | ||||
|  | ||||
| 		createdRecord := createdRecords[0] | ||||
|  | ||||
| 		tableCol := []string{"type", "name", "value", "TTL", "priority"} | ||||
| 		table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 		value := createdRecord.Value | ||||
| 		if len(createdRecord.Value) > 30 { | ||||
| 			value = fmt.Sprintf("%s...", createdRecord.Value[:30]) | ||||
| 		} | ||||
|  | ||||
| 		table.Append([]string{ | ||||
| 			createdRecord.Type, | ||||
| 			createdRecord.Name, | ||||
| 			value, | ||||
| 			createdRecord.TTL.String(), | ||||
| 			strconv.Itoa(createdRecord.Priority), | ||||
| 		}) | ||||
|  | ||||
| 		table.Render() | ||||
|  | ||||
| 		logrus.Info("record created") | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										38
									
								
								cli/domain/domain.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								cli/domain/domain.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| package domain | ||||
|  | ||||
| import ( | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // DomainCommand supports managing DNS entries. | ||||
| var DomainCommand = &cli.Command{ | ||||
| 	Name:      "domain", | ||||
| 	Usage:     "Manage domains via 3rd party providers", | ||||
| 	Aliases:   []string{"d"}, | ||||
| 	ArgsUsage: "<domain>", | ||||
| 	Description: ` | ||||
| This command supports managing domain name records via 3rd party providers such | ||||
| as Gandi DNS. It supports listing, creating and removing all types of records | ||||
| that you might need for managing Co-op Cloud apps. | ||||
|  | ||||
| The following providers are supported: | ||||
|  | ||||
|     Gandi DNS https://www.gandi.net | ||||
|  | ||||
| You need an account with such a provider already. Typically, you need to | ||||
| provide an API token on the Abra command-line when using these commands so that | ||||
| you can authenticate with your provider account. | ||||
|  | ||||
| Any new provider can be integrated, we welcome change sets. See the underlying | ||||
| DNS library documentation for more. It supports many existing providers and | ||||
| allows to implement new provider support easily. | ||||
|  | ||||
|     https://pkg.go.dev/github.com/libdns/libdns | ||||
|  | ||||
| `, | ||||
| 	Subcommands: []*cli.Command{ | ||||
| 		DomainListCommand, | ||||
| 		DomainCreateCommand, | ||||
| 		DomainRemoveCommand, | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										76
									
								
								cli/domain/list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								cli/domain/list.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| package domain | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	gandiPkg "coopcloud.tech/abra/pkg/dns/gandi" | ||||
| 	"github.com/libdns/gandi" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // DomainListCommand lists domains. | ||||
| var DomainListCommand = &cli.Command{ | ||||
| 	Name:      "list", | ||||
| 	Usage:     "List domain name records for a zone", | ||||
| 	Aliases:   []string{"ls"}, | ||||
| 	ArgsUsage: "<zone>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.DNSProviderFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command lists all domain name records managed by a 3rd party provider. | ||||
|  | ||||
| You must specify a zone (e.g. example.com) under which your domain name records | ||||
| are listed. This zone must already be created on your provider account. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		zone := c.Args().First() | ||||
| 		if zone == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided")) | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		var provider gandi.Provider | ||||
| 		switch internal.DNSProvider { | ||||
| 		case "gandi": | ||||
| 			provider, err = gandiPkg.New() | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		default: | ||||
| 			logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider) | ||||
| 		} | ||||
|  | ||||
| 		records, err := provider.GetRecords(c.Context, zone) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		tableCol := []string{"type", "name", "value", "TTL", "priority"} | ||||
| 		table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 		for _, record := range records { | ||||
| 			value := record.Value | ||||
| 			if len(record.Value) > 30 { | ||||
| 				value = fmt.Sprintf("%s...", record.Value[:30]) | ||||
| 			} | ||||
|  | ||||
| 			table.Append([]string{ | ||||
| 				record.Type, | ||||
| 				record.Name, | ||||
| 				value, | ||||
| 				record.TTL.String(), | ||||
| 				strconv.Itoa(record.Priority), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		table.Render() | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										127
									
								
								cli/domain/remove.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								cli/domain/remove.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | ||||
| package domain | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	gandiPkg "coopcloud.tech/abra/pkg/dns/gandi" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/libdns/gandi" | ||||
| 	"github.com/libdns/libdns" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // DomainRemoveCommand lists domains. | ||||
| var DomainRemoveCommand = &cli.Command{ | ||||
| 	Name:      "remove", | ||||
| 	Usage:     "Remove domain name records for a zone", | ||||
| 	Aliases:   []string{"rm"}, | ||||
| 	ArgsUsage: "<zone> <type> <name>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.DNSProviderFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command removes a new domain name record for a specific zone. | ||||
|  | ||||
| You must specify a zone (e.g. example.com) under which your domain name records | ||||
| are listed. This zone must already be created on your provider account. | ||||
|  | ||||
| The "<id>" can be retrieved by running "abra domain list <zone>". This can be | ||||
| used then in this command for deletion of the record. | ||||
|  | ||||
| Example: | ||||
|  | ||||
|     abra domain remove -p gandi foo.com A myapp | ||||
|  | ||||
| Which means that the domain name record of type A for myapp.foo.com will be deleted. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		zone := c.Args().First() | ||||
| 		if zone == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided")) | ||||
| 		} | ||||
|  | ||||
| 		recordType := c.Args().Get(1) | ||||
| 		recordName := c.Args().Get(2) | ||||
|  | ||||
| 		if recordType == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no type provided")) | ||||
| 		} | ||||
| 		if recordName == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		var provider gandi.Provider | ||||
| 		switch internal.DNSProvider { | ||||
| 		case "gandi": | ||||
| 			provider, err = gandiPkg.New() | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		default: | ||||
| 			logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider) | ||||
| 		} | ||||
|  | ||||
| 		records, err := provider.GetRecords(c.Context, zone) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		var toDelete libdns.Record | ||||
| 		for _, record := range records { | ||||
| 			if record.Type == recordType && record.Name == recordName { | ||||
| 				toDelete = record | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (libdns.Record{}) == toDelete { | ||||
| 			logrus.Fatal("provider library reports no matching record?") | ||||
| 		} | ||||
|  | ||||
| 		tableCol := []string{"type", "name", "value", "TTL", "priority"} | ||||
| 		table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 		value := toDelete.Value | ||||
| 		if len(toDelete.Value) > 30 { | ||||
| 			value = fmt.Sprintf("%s...", toDelete.Value[:30]) | ||||
| 		} | ||||
|  | ||||
| 		table.Append([]string{ | ||||
| 			toDelete.Type, | ||||
| 			toDelete.Name, | ||||
| 			value, | ||||
| 			toDelete.TTL.String(), | ||||
| 			strconv.Itoa(toDelete.Priority), | ||||
| 		}) | ||||
|  | ||||
| 		table.Render() | ||||
|  | ||||
| 		response := false | ||||
| 		prompt := &survey.Confirm{ | ||||
| 			Message: "continue with record deletion?", | ||||
| 		} | ||||
|  | ||||
| 		if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !response { | ||||
| 			logrus.Fatal("exiting as requested") | ||||
| 		} | ||||
|  | ||||
| 		_, err = provider.DeleteRecords(c.Context, zone, []libdns.Record{toDelete}) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Info("record successfully deleted") | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| } | ||||
| @ -49,3 +49,28 @@ var ForceFlag = &cli.BoolFlag{ | ||||
| 	Aliases:     []string{"f"}, | ||||
| 	Destination: &Force, | ||||
| } | ||||
|  | ||||
| // Chaos engages chaos mode. | ||||
| var Chaos bool | ||||
|  | ||||
| // ChaosFlag turns on/off chaos functionality. | ||||
| var ChaosFlag = &cli.BoolFlag{ | ||||
| 	Name:        "chaos", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"ch"}, | ||||
| 	Usage:       "Deploy uncommitted recipes changes. Use with care!", | ||||
| 	Destination: &Chaos, | ||||
| } | ||||
|  | ||||
| // DNSProvider specifies a DNS provider. | ||||
| var DNSProvider string | ||||
|  | ||||
| // DNSProviderFlag selects a DNS provider. | ||||
| var DNSProviderFlag = &cli.StringFlag{ | ||||
| 	Name:        "provider", | ||||
| 	Value:       "", | ||||
| 	Aliases:     []string{"p"}, | ||||
| 	Usage:       "DNS provider", | ||||
| 	Destination: &DNSProvider, | ||||
| 	Required:    true, | ||||
| } | ||||
|  | ||||
							
								
								
									
										38
									
								
								cli/internal/errors_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								cli/internal/errors_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // Testing functions that call os.Exit | ||||
| // https://stackoverflow.com/questions/26225513/how-to-test-os-exit-scenarios-in-go | ||||
| // https://talks.golang.org/2014/testing.slide#23 | ||||
|  | ||||
| var testapp = &cli.App{ | ||||
| 	Name:  "abra", | ||||
| 	Usage: `The Co-op Cloud command-line utility belt 🎩🐇`, | ||||
| } | ||||
|  | ||||
| // not testing output as that changes. just if it exits with code 1 | ||||
| // does not work because of some weird errors on cli's part. Its a hard lib to test effectively. | ||||
| // func TestShowSubcommandHelpAndError(t *testing.T) { | ||||
| // 	if os.Getenv("HelpAndError") == "1" { | ||||
| // 		ShowSubcommandHelpAndError(cli.NewContext(testapp, nil, nil), errors.New("Test error")) | ||||
| // 		return | ||||
| // 	} | ||||
| // 	cmd := exec.Command(os.Args[0], "-test.run=TestShowSubcommandHelpAndError") | ||||
| // 	cmd.Env = append(os.Environ(), "HelpAndError=1") | ||||
| // 	var out bytes.Buffer | ||||
| // 	cmd.Stderr = &out | ||||
| // 	err := cmd.Run() | ||||
| // 	println(out.String()) | ||||
| // 	if !strings.Contains(out.String(), "Test error") { | ||||
|  | ||||
| // 		t.Fatalf("expected command to show the error causing the exit, did not get correct stdout output") | ||||
| // 	} | ||||
|  | ||||
| // 	if e, ok := err.(*exec.ExitError); ok && !e.Success() { | ||||
| // 		return | ||||
| // 	} | ||||
| // 	t.Fatalf("process ran with err %v, want exit status 1", err) | ||||
| // } | ||||
| @ -31,7 +31,7 @@ var PatchFlag = &cli.BoolFlag{ | ||||
| // RecipeCommand defines all recipe related sub-commands. | ||||
| var RecipeCommand = &cli.Command{ | ||||
| 	Name:      "recipe", | ||||
| 	Usage:     "Manage recipes", | ||||
| 	Usage:     "Manage recipes (for maintainers)", | ||||
| 	ArgsUsage: "<recipe>", | ||||
| 	Aliases:   []string{"r"}, | ||||
| 	Description: ` | ||||
|  | ||||
| @ -120,7 +120,9 @@ or a rollback of an app. | ||||
| 			prompt := &survey.Input{ | ||||
| 				Message: "tag message", | ||||
| 			} | ||||
| 			survey.AskOne(prompt, &TagMessage) | ||||
| 			if err := survey.AskOne(prompt, &TagMessage); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var createTagOptions git.CreateTagOptions | ||||
| @ -140,7 +142,9 @@ or a rollback of an app. | ||||
| 				prompt := &survey.Input{ | ||||
| 					Message: "commit message", | ||||
| 				} | ||||
| 				survey.AskOne(prompt, &CommitMessage) | ||||
| 				if err := survey.AskOne(prompt, &CommitMessage); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 			err = commitWorktree.AddGlob("compose.**yml") | ||||
| 			if err != nil { | ||||
| @ -287,6 +291,9 @@ func getImageVersions(recipe recipe.Recipe) map[string]string { | ||||
|  | ||||
| 	var services = make(map[string]string) | ||||
| 	for _, service := range recipe.Config.Services { | ||||
| 		if service.Image == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		srv := strings.Split(service.Image, ":") | ||||
| 		services[srv[0]] = srv[1] | ||||
| 	} | ||||
|  | ||||
| @ -3,6 +3,7 @@ package server | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"os/exec" | ||||
| 	"os/user" | ||||
| 	"strings" | ||||
|  | ||||
| @ -53,12 +54,13 @@ All communication between Abra and the server will use this SSH connection. | ||||
| 	}, | ||||
| 	ArgsUsage: "<domain> [<user>] [<port>]", | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		if c.Args().Len() == 1 && !local { | ||||
|  | ||||
| 		if c.Args().Len() == 2 && !local { | ||||
| 			err := errors.New("missing arguments <domain> or '--local'") | ||||
| 			internal.ShowSubcommandHelpAndError(c, err) | ||||
| 		} | ||||
|  | ||||
| 		if c.Args().Get(1) != "" && local { | ||||
| 		if c.Args().Get(2) != "" && local { | ||||
| 			err := errors.New("cannot use '<domain>' and '--local' together") | ||||
| 			internal.ShowSubcommandHelpAndError(c, err) | ||||
| 		} | ||||
| @ -69,6 +71,9 @@ All communication between Abra and the server will use this SSH connection. | ||||
| 			if err := server.CreateServerDir(domainName); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if _, err := exec.LookPath("docker"); err != nil { | ||||
| 				return errors.New("docker command not found on $PATH, is it installed?") | ||||
| 			} | ||||
| 			logrus.Info("local server has been added") | ||||
| 			return nil | ||||
| 		} | ||||
| @ -113,6 +118,10 @@ All communication between Abra and the server will use this SSH connection. | ||||
| 		ctx := context.Background() | ||||
| 		cl, err := client.New(domainName) | ||||
| 		if err != nil { | ||||
| 			logrus.Warn("cleaning up context due to connection failure") | ||||
| 			if err := client.DeleteContext(domainName); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -120,6 +129,10 @@ All communication between Abra and the server will use this SSH connection. | ||||
| 			if strings.Contains(err.Error(), "command not found") { | ||||
| 				logrus.Fatalf("docker is not installed on '%s'?", domainName) | ||||
| 			} else { | ||||
| 				logrus.Warn("cleaning up context due to connection failure") | ||||
| 				if err := client.DeleteContext(domainName); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				logrus.Fatalf("unable to make a connection to '%s'?", domainName) | ||||
| 			} | ||||
| 			logrus.Debug(err) | ||||
|  | ||||
| @ -1,17 +1,13 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/libcapsul" | ||||
| 	"github.com/hetznercloud/hcloud-go/hcloud" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| @ -182,8 +178,8 @@ environment variable or otherwise passing the "--env/-e" flag. | ||||
| 		}, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		name := c.Args().First() | ||||
| 		if name == "" { | ||||
| 		capsulName := c.Args().First() | ||||
| 		if capsulName == "" { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) | ||||
| 		} | ||||
|  | ||||
| @ -191,57 +187,17 @@ environment variable or otherwise passing the "--env/-e" flag. | ||||
| 			logrus.Fatal("Capsul API token is missing") | ||||
| 		} | ||||
|  | ||||
| 		// yep, the response time is quite slow, something to fix on the Capsul side | ||||
| 		client := &http.Client{Timeout: 20 * time.Second} | ||||
|  | ||||
| 		capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance) | ||||
| 		logrus.Debugf("using '%s' as capsul create url", capsulCreateURL) | ||||
| 		values := map[string]string{ | ||||
| 			"name":      name, | ||||
| 			"size":      capsulType, | ||||
| 			"os":        capsulImage, | ||||
| 			"ssh_key_0": capsulSSHKey, | ||||
| 		} | ||||
|  | ||||
| 		payload, err := json.Marshal(values) | ||||
| 		capsulClient := libcapsul.New(capsulCreateURL, capsulAPIToken) | ||||
| 		resp, err := capsulClient.Create(capsulName, capsulType, capsulImage, capsulSSHKey) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload)) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		req.Header = http.Header{ | ||||
| 			"Content-Type":  []string{"application/json"}, | ||||
| 			"Authorization": []string{capsulAPIToken}, | ||||
| 		} | ||||
|  | ||||
| 		res, err := client.Do(req) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		defer res.Body.Close() | ||||
|  | ||||
| 		if res.StatusCode != http.StatusOK { | ||||
| 			body, err := ioutil.ReadAll(res.Body) | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 			logrus.Fatal(string(body)) | ||||
| 		} | ||||
|  | ||||
| 		type capsulCreateResponse struct{ ID string } | ||||
| 		var resp capsulCreateResponse | ||||
| 		if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		logrus.Debugf("capsul created with ID: '%s'", resp.ID) | ||||
|  | ||||
| 		tableColumns := []string{"Name", "ID"} | ||||
| 		table := formatter.CreateTable(tableColumns) | ||||
| 		table.Append([]string{name, resp.ID}) | ||||
| 		table.Append([]string{capsulName, resp.ID}) | ||||
| 		table.Render() | ||||
|  | ||||
| 		return nil | ||||
|  | ||||
| @ -8,7 +8,7 @@ import ( | ||||
| var ServerCommand = &cli.Command{ | ||||
| 	Name:    "server", | ||||
| 	Aliases: []string{"s"}, | ||||
| 	Usage:   "Manage servers", | ||||
| 	Usage:   "Manage servers via 3rd party providers", | ||||
| 	Description: ` | ||||
| Manage the lifecycle of a server. | ||||
|  | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| package cli | ||||
|  | ||||
| import ( | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| // VersionCommand prints the version of abra. | ||||
| var VersionCommand = &cli.Command{ | ||||
| 	Name:  "version", | ||||
| 	Usage: "Print version", | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		cli.VersionPrinter(c) | ||||
| 		return nil | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @ -25,6 +25,7 @@ require ( | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d | ||||
| 	github.com/Microsoft/hcsshim v0.8.21 // indirect | ||||
| 	github.com/containerd/containerd v1.5.5 // indirect | ||||
| 	github.com/docker/docker-credential-helpers v0.6.4 // indirect | ||||
| @ -32,9 +33,12 @@ require ( | ||||
| 	github.com/fvbommel/sortorder v1.0.2 // indirect | ||||
| 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | ||||
| 	github.com/gorilla/mux v1.8.0 // indirect | ||||
| 	github.com/libdns/gandi v1.0.2 | ||||
| 	github.com/libdns/libdns v0.2.1 | ||||
| 	github.com/moby/sys/mount v0.2.0 // indirect | ||||
| 	github.com/morikuni/aec v1.0.0 // indirect | ||||
| 	github.com/opencontainers/runc v1.0.2 // indirect | ||||
| 	github.com/theupdateframework/notary v0.7.0 // indirect | ||||
| 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 | ||||
| ) | ||||
|  | ||||
							
								
								
									
										7
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.sum
									
									
									
									
									
								
							| @ -21,6 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA | ||||
| cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= | ||||
| cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= | ||||
| cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= | ||||
| coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d h1:5A69AFx2BP5J43Y9SaB9LlAIMLr2SWqbzfgjUh8sgKM= | ||||
| coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d/go.mod h1:HEQ9pSJRsDKabMxPfYCCzpVpAreLoC4Gh4SkVyOaKvk= | ||||
| coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0= | ||||
| coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= | ||||
| dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||
| @ -510,6 +512,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/libdns/gandi v1.0.2 h1:1Ts8UpI1x5PVKpOjKC7Dn4+EObndz9gm6vdZnloHSKQ= | ||||
| github.com/libdns/gandi v1.0.2/go.mod h1:hxpbQKcQFgQrTS5lV4tAgn6QoL6HcCnoBJaW5nOW4Sk= | ||||
| github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= | ||||
| github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= | ||||
| github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= | ||||
| github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||||
| github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= | ||||
| github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||||
|  | ||||
							
								
								
									
										4
									
								
								pkg/client/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								pkg/client/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| IMPORTANT POINT ABOUT CONTEXTS | ||||
|  | ||||
| Please use context names starting with `testContext` for testing purposes to ensure that no data is lost. such as `testContext`, `testContext2`, `testContextFail` etc | ||||
|  | ||||
| @ -4,37 +4,44 @@ package client | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/docker/cli/cli/connhelper" | ||||
| 	"github.com/docker/docker/client" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // New initiates a new Docker client. | ||||
| func New(contextName string) (*client.Client, error) { | ||||
| 	context, err := GetContext(contextName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	ctxEndpoint, err := GetContextEndpoint(context) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	helper := newConnectionHelper(ctxEndpoint) | ||||
| 	httpClient := &http.Client{ | ||||
| 		// No tls, no proxy | ||||
| 		Transport: &http.Transport{ | ||||
| 			DialContext: helper.Dialer, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	var clientOpts []client.Opt | ||||
| 	clientOpts = append(clientOpts, | ||||
| 		client.WithHTTPClient(httpClient), | ||||
| 		client.WithHost(helper.Host), | ||||
| 		client.WithDialContext(helper.Dialer), | ||||
| 	) | ||||
|  | ||||
| 	clientOpts = append(clientOpts, client.WithTimeout(3*time.Second)) | ||||
|  | ||||
| 	if contextName != "default" { | ||||
| 		context, err := GetContext(contextName) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		ctxEndpoint, err := GetContextEndpoint(context) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		helper := newConnectionHelper(ctxEndpoint) | ||||
| 		httpClient := &http.Client{ | ||||
| 			// No tls, no proxy | ||||
| 			Transport: &http.Transport{ | ||||
| 				DialContext: helper.Dialer, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		clientOpts = append(clientOpts, | ||||
| 			client.WithHTTPClient(httpClient), | ||||
| 			client.WithHost(helper.Host), | ||||
| 			client.WithDialContext(helper.Dialer), | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	version := os.Getenv("DOCKER_API_VERSION") | ||||
| 	if version != "" { | ||||
| @ -45,10 +52,18 @@ func New(contextName string) (*client.Client, error) { | ||||
|  | ||||
| 	cl, err := client.NewClientWithOpts(clientOpts...) | ||||
| 	if err != nil { | ||||
| 		logrus.Fatalf("unable to create Docker client: %s", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("created client for '%s'", contextName) | ||||
|  | ||||
| 	return cl, nil | ||||
| } | ||||
|  | ||||
| func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper { | ||||
| 	helper, err := connhelper.GetConnectionHelper(daemonURL) | ||||
| 	if err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
| 	return helper | ||||
| } | ||||
|  | ||||
							
								
								
									
										46
									
								
								pkg/client/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								pkg/client/client_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| package client_test | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| ) | ||||
|  | ||||
| // use at the start to ensure testContext[0, 1, ..., amnt-1] exist and | ||||
| // testContextFail[0, 1, ..., failAmnt-1] don't exist | ||||
| func ensureTestState(amnt, failAmnt int) error { | ||||
| 	for i := 0; i < amnt; i++ { | ||||
| 		err := client.CreateContext(fmt.Sprintf("testContext%d", i), "", "") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	for i := 0; i < failAmnt; i++ { | ||||
| 		if _, er := client.GetContext(fmt.Sprintf("testContextFail%d", i)); er == nil { | ||||
| 			err := client.DeleteContext(fmt.Sprintf("testContextFail%d", i)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	err := ensureTestState(1, 1) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Couldn't ensure existence/nonexistence of contexts: %s", err) | ||||
| 	} | ||||
| 	contextName := "testContext0" | ||||
| 	_, err = client.New(contextName) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("couldn't initialise a new client with context %s: %s", contextName, err) | ||||
| 	} | ||||
| 	contextName = "testContextFail0" | ||||
| 	_, err = client.New(contextName) | ||||
| 	if err == nil { | ||||
| 		t.Errorf("client.New(\"testContextFail0\") should have failed but didn't return an error") | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @ -1,45 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"github.com/docker/cli/cli/connhelper" | ||||
| 	"github.com/docker/cli/cli/context/docker" | ||||
| 	dCliContextStore "github.com/docker/cli/cli/context/store" | ||||
| 	dClient "github.com/docker/docker/client" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper { | ||||
| 	helper, err := connhelper.GetConnectionHelper(daemonURL) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
| 	return helper | ||||
| } | ||||
|  | ||||
| func getDockerEndpoint(host string) (docker.Endpoint, error) { | ||||
| 	skipTLSVerify := false | ||||
| 	ep := docker.Endpoint{ | ||||
| 		EndpointMeta: docker.EndpointMeta{ | ||||
| 			Host:          host, | ||||
| 			SkipTLSVerify: skipTLSVerify, | ||||
| 		}, | ||||
| 	} | ||||
| 	// try to resolve a docker client, validating the configuration | ||||
| 	opts, err := ep.ClientOpts() | ||||
| 	if err != nil { | ||||
| 		return docker.Endpoint{}, err | ||||
| 	} | ||||
| 	if _, err := dClient.NewClientWithOpts(opts...); err != nil { | ||||
| 		return docker.Endpoint{}, err | ||||
| 	} | ||||
| 	return ep, nil | ||||
| } | ||||
|  | ||||
| func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) { | ||||
| 	ep, err := getDockerEndpoint(host) | ||||
| 	if err != nil { | ||||
| 		return docker.EndpointMeta{}, nil, err | ||||
| 	} | ||||
| 	return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil | ||||
| } | ||||
| @ -9,6 +9,9 @@ import ( | ||||
| 	context "github.com/docker/cli/cli/context" | ||||
| 	"github.com/docker/cli/cli/context/docker" | ||||
| 	contextStore "github.com/docker/cli/cli/context/store" | ||||
| 	dCliContextStore "github.com/docker/cli/cli/context/store" | ||||
| 	cliflags "github.com/docker/cli/cli/flags" | ||||
| 	dClient "github.com/docker/docker/client" | ||||
| 	"github.com/moby/term" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
| @ -94,7 +97,6 @@ func GetContext(contextName string) (contextStore.Metadata, error) { | ||||
| } | ||||
|  | ||||
| func GetContextEndpoint(ctx contextStore.Metadata) (string, error) { | ||||
| 	// safe to use docker key hardcoded since abra doesn't use k8s... yet... | ||||
| 	endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase) | ||||
| 	if !ok { | ||||
| 		err := errors.New("context lacks Docker endpoint") | ||||
| @ -108,21 +110,47 @@ func newContextStore(dir string, config contextStore.Config) contextStore.Store | ||||
| } | ||||
|  | ||||
| func NewDefaultDockerContextStore() *command.ContextStoreWithDefault { | ||||
| 	// Grabbing the stderr from Docker commands | ||||
| 	// Much easier to fit this into the code we are using to replicate docker cli commands | ||||
| 	_, _, stderr := term.StdStreams() | ||||
| 	// TODO: Look into custom docker configs in case users want that | ||||
| 	dockerConfig := dConfig.LoadDefaultConfigFile(stderr) | ||||
| 	contextDir := dConfig.ContextStoreDir() | ||||
| 	storeConfig := command.DefaultContextStoreConfig() | ||||
| 	store := newContextStore(contextDir, storeConfig) | ||||
|  | ||||
| 	opts := &cliflags.CommonOptions{Context: "default"} | ||||
|  | ||||
| 	dockerContextStore := &command.ContextStoreWithDefault{ | ||||
| 		Store: store, | ||||
| 		Resolver: func() (*command.DefaultContext, error) { | ||||
| 			// nil for the Opts because it works without it and its a cli thing | ||||
| 			return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr) | ||||
| 			return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	return dockerContextStore | ||||
| } | ||||
|  | ||||
| func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) { | ||||
| 	ep, err := getDockerEndpoint(host) | ||||
| 	if err != nil { | ||||
| 		return docker.EndpointMeta{}, nil, err | ||||
| 	} | ||||
| 	return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil | ||||
| } | ||||
|  | ||||
| func getDockerEndpoint(host string) (docker.Endpoint, error) { | ||||
| 	skipTLSVerify := false | ||||
| 	ep := docker.Endpoint{ | ||||
| 		EndpointMeta: docker.EndpointMeta{ | ||||
| 			Host:          host, | ||||
| 			SkipTLSVerify: skipTLSVerify, | ||||
| 		}, | ||||
| 	} | ||||
| 	// try to resolve a docker client, validating the configuration | ||||
| 	opts, err := ep.ClientOpts() | ||||
| 	if err != nil { | ||||
| 		return docker.Endpoint{}, err | ||||
| 	} | ||||
| 	if _, err := dClient.NewClientWithOpts(opts...); err != nil { | ||||
| 		return docker.Endpoint{}, err | ||||
| 	} | ||||
| 	return ep, nil | ||||
| } | ||||
|  | ||||
| @ -30,6 +30,34 @@ func dockerContext(host, key string) TestContext { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateContext(t *testing.T) { | ||||
| 	err := client.CreateContext("testContext0", "wronguser", "wrongport") | ||||
| 	if err == nil { | ||||
| 		t.Error("client.CreateContext(\"testContextCreate\", \"wronguser\", \"wrongport\") should have failed but didn't return an error") | ||||
| 	} | ||||
| 	err = client.CreateContext("testContext0", "", "") | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Couldn't create context: %s", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDeleteContext(t *testing.T) { | ||||
| 	ensureTestState(1, 1) | ||||
| 	err := client.DeleteContext("default") | ||||
| 	if err == nil { | ||||
| 		t.Errorf("client.DeleteContext(\"default\") should have failed but didn't return an error") | ||||
| 	} | ||||
|  | ||||
| 	err = client.DeleteContext("testContext0") | ||||
| 	if err != nil { | ||||
| 		t.Errorf("client.DeleteContext(\"testContext0\") failed: %s", err) | ||||
| 	} | ||||
| 	err = client.DeleteContext("testContextFail0") | ||||
| 	if err == nil { | ||||
| 		t.Errorf("client.DeleteContext(\"testContextFail0\") should have failed (attempt to delete non-existent context) but didn't return an error") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetContextEndpoint(t *testing.T) { | ||||
| 	var testDockerContexts = []TestContext{ | ||||
| 		dockerContext("ssh://foobar", "docker"), | ||||
|  | ||||
							
								
								
									
										28
									
								
								pkg/dns/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/dns/common.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package dns | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // NewToken constructs a new DNS provider token. | ||||
| func NewToken(provider, providerTokenEnvVar string) (string, error) { | ||||
| 	if token, present := os.LookupEnv(providerTokenEnvVar); present { | ||||
| 		return token, nil | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("no %s in environment, asking via stdin", providerTokenEnvVar) | ||||
|  | ||||
| 	var token string | ||||
| 	prompt := &survey.Input{ | ||||
| 		Message: fmt.Sprintf("%s API token?", provider), | ||||
| 	} | ||||
| 	if err := survey.AskOne(prompt, &token); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return token, nil | ||||
| } | ||||
							
								
								
									
										15
									
								
								pkg/dns/gandi/gandi.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								pkg/dns/gandi/gandi.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| package gandi | ||||
|  | ||||
| import ( | ||||
| 	"coopcloud.tech/abra/pkg/dns" | ||||
| 	"github.com/libdns/gandi" | ||||
| ) | ||||
|  | ||||
| // New constructs a new DNs provider. | ||||
| func New() (gandi.Provider, error) { | ||||
| 	token, err := dns.NewToken("Gandi", "GANDI_TOKEN") | ||||
| 	if err != nil { | ||||
| 		return gandi.Provider{}, err | ||||
| 	} | ||||
| 	return gandi.Provider{APIToken: token}, nil | ||||
| } | ||||
							
								
								
									
										48
									
								
								pkg/git/read.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								pkg/git/read.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| package git | ||||
|  | ||||
| import ( | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"github.com/go-git/go-git/v5" | ||||
| 	"github.com/go-git/go-git/v5/plumbing" | ||||
| ) | ||||
|  | ||||
| // GetRecipeHead retrieves latest HEAD metadata. | ||||
| func GetRecipeHead(recipeName string) (*plumbing.Reference, error) { | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) | ||||
|  | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	head, err := repo.Head() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return head, nil | ||||
| } | ||||
|  | ||||
| // IsClean checks if a repo has unstaged changes | ||||
| func IsClean(recipeName string) (bool, error) { | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) | ||||
|  | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	worktree, err := repo.Worktree() | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	status, err := worktree.Status() | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	return status.IsClean(), nil | ||||
| } | ||||
| @ -109,6 +109,15 @@ func EnsureExists(recipe string) error { | ||||
| func EnsureVersion(recipeName, version string) error { | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) | ||||
|  | ||||
| 	isClean, err := gitPkg.IsClean(recipeName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !isClean { | ||||
| 		return fmt.Errorf("'%s' has locally unstaged changes", recipeName) | ||||
| 	} | ||||
|  | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @ -195,3 +204,26 @@ func EnsureLatest(recipeName string) error { | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ChaosVersion constructs a chaos mode recipe version. | ||||
| func ChaosVersion(recipeName string) (string, error) { | ||||
| 	var version string | ||||
|  | ||||
| 	head, err := gitPkg.GetRecipeHead(recipeName) | ||||
| 	if err != nil { | ||||
| 		return version, err | ||||
| 	} | ||||
|  | ||||
| 	version = head.String()[:8] | ||||
|  | ||||
| 	isClean, err := gitPkg.IsClean(recipeName) | ||||
| 	if err != nil { | ||||
| 		return version, err | ||||
| 	} | ||||
|  | ||||
| 	if !isClean { | ||||
| 		version = fmt.Sprintf("%s + unstaged changes", version) | ||||
| 	} | ||||
|  | ||||
| 	return version, nil | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| ABRA_VERSION="0.2.0-alpha" | ||||
| ABRA_VERSION="0.2.2-alpha" | ||||
| ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" | ||||
|  | ||||
| function show_banner { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user