Adds support for ARGO_CD_[TARGET_REVISION|REVISION] and pass to Custom Tool/Helm/Jsonnet (#2415)

This commit is contained in:
Alex Collins 2019-10-21 16:54:23 -07:00 committed by GitHub
parent bbfb96cb01
commit 5706a17155
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 462 additions and 151 deletions

View file

@ -221,3 +221,20 @@ release-precheck: manifests
.PHONY: release
release: pre-commit release-precheck image release-cli
.PHONY: build-docs
build-docs:
mkdocs build
.PHONY: serve-docs
serve-docs:
mkdocs serve
.PHONY: lint-docs
lint-docs:
# https://github.com/dkhamsing/awesome_bot
find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat white-list | grep -v "#" | tr "\n" ','` --skip-save-results --
.PHONY: publish-docs
publish-docs: lint-docs
mkdocs gh-deploy

View file

@ -518,6 +518,10 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaStr, false)
case "jsonnet-tla-code":
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaCode, true)
case "jsonnet-ext-var-str":
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarStr, false)
case "jsonnet-ext-var-code":
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarCode, true)
case "sync-policy":
switch appOpts.syncPolicy {
case "automated":
@ -645,7 +649,15 @@ func setJsonnetOpt(src *argoappv1.ApplicationSource, tlaParameters []string, cod
if src.Directory.IsZero() {
src.Directory = nil
}
}
func setJsonnetOptExtVar(src *argoappv1.ApplicationSource, jsonnetExtVar []string, code bool) {
if src.Directory == nil {
src.Directory = &argoappv1.ApplicationSourceDirectory{}
}
for _, j := range jsonnetExtVar {
src.Directory.Jsonnet.ExtVars = append(src.Directory.Jsonnet.ExtVars, argoappv1.NewJsonnetVar(j, code))
}
}
type appOptions struct {
@ -671,6 +683,8 @@ type appOptions struct {
configManagementPlugin string
jsonnetTlaStr []string
jsonnetTlaCode []string
jsonnetExtVarStr []string
jsonnetExtVarCode []string
kustomizeImages []string
}
@ -697,6 +711,8 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
command.Flags().StringVar(&opts.configManagementPlugin, "config-management-plugin", "", "Config management plugin name")
command.Flags().StringArrayVar(&opts.jsonnetTlaStr, "jsonnet-tla-str", []string{}, "Jsonnet top level string arguments")
command.Flags().StringArrayVar(&opts.jsonnetTlaCode, "jsonnet-tla-code", []string{}, "Jsonnet top level code arguments")
command.Flags().StringArrayVar(&opts.jsonnetExtVarStr, "jsonnet-ext-var-str", []string{}, "Jsonnet string ext var")
command.Flags().StringArrayVar(&opts.jsonnetExtVarCode, "jsonnet-ext-var-code", []string{}, "Jsonnet ext var")
command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d)")
}
@ -815,11 +831,12 @@ func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion
}
func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions) []string {
res, err := repository.GenerateManifests(local, &repoapiclient.ManifestRequest{
ApplicationSource: &app.Spec.Source,
res, err := repository.GenerateManifests(local, app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
Repo: &argoappv1.Repository{Repo: app.Spec.Source.RepoURL},
AppLabelKey: appLabelKey,
AppLabelValue: app.Name,
Namespace: app.Spec.Destination.Namespace,
ApplicationSource: &app.Spec.Source,
KustomizeOptions: kustomizeOptions,
KubeVersion: kubeVersion,
})

View file

@ -20,7 +20,7 @@ $ curl $ARGOCD_SERVER/api/v1/applications --cookie "argocd.token=$ARGOCD_TOKEN"
{"metadata":{"selfLink":"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications","resourceVersion":"37755"},"items":...}
```
> >v1.3
> v1.3
Then pass using the HTTP `Authorization` header, prefixing with `Bearer `:
@ -28,5 +28,4 @@ Then pass using the HTTP `Authorization` header, prefixing with `Bearer `:
$ curl $ARGOCD_SERVER/api/v1/applications -H "Authorization: Bearer $ARGOCD_TOKEN"
{"metadata":{"selfLink":"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications","resourceVersion":"37755"},"items":...}
```
You sh

View file

@ -7,19 +7,19 @@ The web site is build using `mkdocs` and `mkdocs-material`.
To test:
```bash
mkdocs serve
make serve-docs
```
Check for broken external links:
```bash
find docs -name '*.md' -exec grep -l http {} + | xargs awesome_bot -t 3 --allow-dupe --allow-redirect -w argocd.example.com:443,argocd.example.com,kubernetes.default.svc:443,kubernetes.default.svc,mycluster.com,https://github.com/argoproj/my-private-repository,192.168.0.20,storage.googleapis.com,localhost:8080,localhost:6443,your-kubernetes-cluster-addr,10.97.164.88 --skip-save-results --
make lint-docs
```
## Deploying
```bash
mkdocs gh-deploy
make publish-docs
```
## Analytics

View file

@ -7,7 +7,7 @@ Argo CD supports several different ways in which Kubernetes manifests can be def
* [Kustomize](kustomize.md) applications
* [Helm](helm.md) charts
* [Ksonnet](ksonnet.md) applications
* A directory of YAML/JSON/Jsonnet manifests
* A directory of YAML/JSON/Jsonnet manifests, including [Jsonnet](jsonnet.md).
* Any [custom config management tool](config-management-plugins.md) configured as a config management plugin
## Development

View file

@ -0,0 +1,10 @@
# Build Environment
[Custom tools](config-management-plugins.md), [Helm](helm.md), and [Jsonnet](jsonnet.md) support the following build env vars:
* `ARGOCD_APP_NAME` - name of application
* `ARGOCD_APP_NAMESPACE` - destination application namespace.
* `ARGOCD_APP_REVISION` - the resolved revision, e.g. `f913b6cbf58aa5ae5ca1f8a2b149477aebcbd9d8`
* `ARGOCD_APP_SOURCE_PATH` - the path of the app within the repo
* `ARGOCD_APP_SOURCE_REPO_URL` the repo's URL
* `ARGOCD_APP_SOURCE_TARGET_REVISION` - the target revision from the spec, e.g. `master`.

View file

@ -31,13 +31,9 @@ More config management plugin examples are available in [argocd-example-apps](ht
Commands have access to
(1) The system environment variables
(2) Argo CD environment variables:
* `ARGOCD_APP_NAME` - name of application
* `ARGOCD_APP_NAMESPACE` - destination application namespace.
(3) Variables in the application spec:
1. The system environment variables
2. [Standard build environment](build-environment.md)
3. Variables in the application spec:
> v1.2

View file

@ -113,3 +113,25 @@ value, in the values.yaml such that the value is stable between each comparison.
```bash
argocd app set redis -p password=abc123
```
## Build Environment
Helm apps have access to the [standard build environment](build-environment.md) via substitution as parameters.
E.g. via the CLI:
```bash
argocd app create APPNAME \
--helm-set-string 'app=${ARGOCD_APP_NAME}'
```
Or via declarative syntax:
```yaml
spec:
source:
helm:
parameters:
- name: app
value: $ARGOCD_APP_NAME
```

View file

@ -0,0 +1,28 @@
# Jsonnet
Any file matching `*.jsonnet` in a directory app is treated as a Jsonnet file.
## Build Environment
Jsonnet apps have access to the [standard build environment](build-environment.md) via substitution into *TLAs* and *external variables*.
E.g. via the CLI:
```bash
argocd app create APPNAME \
--jsonnet-ext-str 'app=${ARGOCD_APP_NAME}' \
--jsonnet-tla-str 'ns=${ARGOCD_APP_NAMESPACE}'
```
Or by declarative syntax:
```yaml
directory:
jsonnet:
extVars:
- name: app
value: $ARGOCD_APP_NAME
tlas:
- name: ns
value: $ARGOCD_APP_NAMESPACE
```

View file

@ -1,5 +1,7 @@
# Ksonnet
!!! tip Warning "Ksonnet is defunct and no longer supported."
## Environments
Ksonnet has a first class concept of an "environment." To create an application from a ksonnet
app directory, an environment must be specified. For example, the following command creates the
@ -32,4 +34,6 @@ When overriding ksonnet parameters in Argo CD, the component name should also be
argocd app set guestbook-default -p guestbook-ui=image=gcr.io/heptio-images/ks-guestbook-demo:0.1
```
## Build Environment
We do not support the [standard build environment](build-environment.md) for Ksonnet.

View file

@ -35,3 +35,7 @@ metadata:
data:
kustomize.buildOptions: --load_restrictor none
```
## Build Environment
Kustomize does not support parameters and therefore cannot support the standard [build environment](build-environment.md).

View file

@ -47,6 +47,7 @@ nav:
- user-guide/kustomize.md
- user-guide/helm.md
- user-guide/ksonnet.md
- user-guide/jsonnet.md
- user-guide/config-management-plugins.md
- user-guide/tool_detection.md
- user-guide/projects.md
@ -57,6 +58,7 @@ nav:
- user-guide/compare-options.md
- user-guide/sync-options.md
- user-guide/parameters.md
- user-guide/build-environment.md
- user-guide/tracking_strategies.md
- user-guide/resource_hooks.md
- user-guide/selective_sync.md

View file

@ -98,6 +98,17 @@ func (e Env) Environ() []string {
return environ
}
// does an operation similar to `envstubst` tool,
// but unlike envsubst it does not change missing names into empty string
// see https://linux.die.net/man/1/envsubst
func (e Env) Envsubst(s string) string {
for _, v := range e {
s = strings.ReplaceAll(s, fmt.Sprintf("$%s", v.Name), v.Value)
s = strings.ReplaceAll(s, fmt.Sprintf("${%s}", v.Name), v.Value)
}
return s
}
// ApplicationSource contains information about github repository, path within repository and target application environment.
type ApplicationSource struct {
// RepoURL is the repository URL of the application manifests
@ -277,6 +288,15 @@ type JsonnetVar struct {
Code bool `json:"code,omitempty" protobuf:"bytes,3,opt,name=code"`
}
func NewJsonnetVar(s string, code bool) JsonnetVar {
parts := strings.SplitN(s, "=", 2)
if len(parts) == 2 {
return JsonnetVar{Name: parts[0], Value: parts[1], Code: code}
} else {
return JsonnetVar{Name: s, Code: code}
}
}
// ApplicationSourceJsonnet holds jsonnet specific options
type ApplicationSourceJsonnet struct {
// ExtVars is a list of Jsonnet External Variables

View file

@ -1002,6 +1002,13 @@ func TestEnv_IsZero(t *testing.T) {
}
}
func TestEnv_Envsubst(t *testing.T) {
env := Env{&EnvEntry{"FOO", "bar"}}
assert.Equal(t, "", env.Envsubst(""))
assert.Equal(t, "bar", env.Envsubst("$FOO"))
assert.Equal(t, "bar", env.Envsubst("${FOO}"))
}
func TestEnv_Environ(t *testing.T) {
tests := []struct {
name string
@ -1442,6 +1449,13 @@ func newTestApp() *Application {
return a
}
func TestNewJsonnetVar(t *testing.T) {
assert.Equal(t, JsonnetVar{}, NewJsonnetVar("", false))
assert.Equal(t, JsonnetVar{Name: "a"}, NewJsonnetVar("a=", false))
assert.Equal(t, JsonnetVar{Name: "a", Code: true}, NewJsonnetVar("a=", true))
assert.Equal(t, JsonnetVar{Name: "a", Value: "b", Code: true}, NewJsonnetVar("a=b", true))
}
func testCond(t ApplicationConditionType, msg string, lastTransitionTime *metav1.Time) ApplicationCondition {
return ApplicationCondition{
Type: t,

View file

@ -40,11 +40,6 @@ import (
"github.com/argoproj/argo-cd/util/text"
)
const (
PluginEnvAppName = "ARGOCD_APP_NAME"
PluginEnvAppNamespace = "ARGOCD_APP_NAMESPACE"
)
// Service implements ManifestService interface
type Service struct {
repoLock *util.KeyLock
@ -190,7 +185,7 @@ func (s *Service) GenerateManifest(c context.Context, q *apiclient.ManifestReque
}
err := s.runRepoOperation(c, q.Repo, q.ApplicationSource, getCached, func(appPath string, revision string) error {
var err error
res, err = GenerateManifests(appPath, q)
res, err = GenerateManifests(appPath, revision, q)
if err != nil {
return err
}
@ -212,7 +207,7 @@ func getHelmRepos(repositories []*v1alpha1.Repository) []helm.HelmRepository {
return repos
}
func helmTemplate(appPath string, q *apiclient.ManifestRequest) ([]*unstructured.Unstructured, error) {
func helmTemplate(appPath string, env *v1alpha1.Env, q *apiclient.ManifestRequest) ([]*unstructured.Unstructured, error) {
templateOpts := &helm.TemplateOpts{
Name: q.AppLabelValue,
Namespace: q.Namespace,
@ -250,7 +245,12 @@ func helmTemplate(appPath string, q *apiclient.ManifestRequest) ([]*unstructured
if templateOpts.Name == "" {
templateOpts.Name = q.AppLabelValue
}
for i, j := range templateOpts.Set {
templateOpts.Set[i] = env.Envsubst(j)
}
for i, j := range templateOpts.SetString {
templateOpts.SetString[i] = env.Envsubst(j)
}
h, err := helm.NewHelmApp(appPath, getHelmRepos(q.Repos))
if err != nil {
return nil, err
@ -278,31 +278,36 @@ func helmTemplate(appPath string, q *apiclient.ManifestRequest) ([]*unstructured
}
// GenerateManifests generates manifests from a path
func GenerateManifests(appPath string, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
func GenerateManifests(appPath, revision string, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
var targetObjs []*unstructured.Unstructured
var dest *v1alpha1.ApplicationDestination
appSourceType, err := GetAppSourceType(q.ApplicationSource, appPath)
if err != nil {
return nil, err
}
repoURL := ""
if q.Repo != nil {
repoURL = q.Repo.Repo
}
env := newEnv(q, revision)
switch appSourceType {
case v1alpha1.ApplicationSourceTypeKsonnet:
targetObjs, dest, err = ksShow(q.AppLabelKey, appPath, q.ApplicationSource.Ksonnet)
case v1alpha1.ApplicationSourceTypeHelm:
targetObjs, err = helmTemplate(appPath, q)
targetObjs, err = helmTemplate(appPath, env, q)
case v1alpha1.ApplicationSourceTypeKustomize:
k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(), repoURL)
targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions)
case v1alpha1.ApplicationSourceTypePlugin:
targetObjs, err = runConfigManagementPlugin(appPath, q, q.Repo.GetGitCreds())
targetObjs, err = runConfigManagementPlugin(appPath, env, q, q.Repo.GetGitCreds())
case v1alpha1.ApplicationSourceTypeDirectory:
var directory *v1alpha1.ApplicationSourceDirectory
if directory = q.ApplicationSource.Directory; directory == nil {
directory = &v1alpha1.ApplicationSourceDirectory{}
}
targetObjs, err = findManifests(appPath, *directory)
targetObjs, err = findManifests(appPath, env, *directory)
}
if err != nil {
return nil, err
@ -355,6 +360,17 @@ func GenerateManifests(appPath string, q *apiclient.ManifestRequest) (*apiclient
return &res, nil
}
func newEnv(q *apiclient.ManifestRequest, revision string) *v1alpha1.Env {
return &v1alpha1.Env{
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: q.AppLabelValue},
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: q.Namespace},
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: revision},
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: q.Repo.Repo},
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: q.ApplicationSource.Path},
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: q.ApplicationSource.TargetRevision},
}
}
// GetAppSourceType returns explicit application source type or examines a directory and determines its application source type
func GetAppSourceType(source *v1alpha1.ApplicationSource, path string) (v1alpha1.ApplicationSourceType, error) {
appSourceType, err := source.ExplicitType()
@ -426,7 +442,7 @@ func ksShow(appLabelKey, appPath string, ksonnetOpts *v1alpha1.ApplicationSource
var manifestFile = regexp.MustCompile(`^.*\.(yaml|yml|json|jsonnet)$`)
// findManifests looks at all yaml files in a directory and unmarshals them into a list of unstructured objects
func findManifests(appPath string, directory v1alpha1.ApplicationSourceDirectory) ([]*unstructured.Unstructured, error) {
func findManifests(appPath string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory) ([]*unstructured.Unstructured, error) {
var objs []*unstructured.Unstructured
err := filepath.Walk(appPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
@ -455,7 +471,7 @@ func findManifests(appPath string, directory v1alpha1.ApplicationSourceDirectory
}
objs = append(objs, &obj)
} else if strings.HasSuffix(f.Name(), ".jsonnet") {
vm := makeJsonnetVm(directory.Jsonnet)
vm := makeJsonnetVm(directory.Jsonnet, env)
vm.Importer(&jsonnet.FileImporter{
JPaths: []string{appPath},
})
@ -499,9 +515,14 @@ func findManifests(appPath string, directory v1alpha1.ApplicationSourceDirectory
return objs, nil
}
func makeJsonnetVm(sourceJsonnet v1alpha1.ApplicationSourceJsonnet) *jsonnet.VM {
func makeJsonnetVm(sourceJsonnet v1alpha1.ApplicationSourceJsonnet, env *v1alpha1.Env) *jsonnet.VM {
vm := jsonnet.MakeVM()
for i, j := range sourceJsonnet.TLAs {
sourceJsonnet.TLAs[i].Value = env.Envsubst(j.Value)
}
for i, j := range sourceJsonnet.ExtVars {
sourceJsonnet.ExtVars[i].Value = env.Envsubst(j.Value)
}
for _, arg := range sourceJsonnet.TLAs {
if arg.Code {
vm.TLACode(arg.Name, arg.Value)
@ -539,12 +560,12 @@ func findPlugin(plugins []*v1alpha1.ConfigManagementPlugin, name string) *v1alph
return nil
}
func runConfigManagementPlugin(appPath string, q *apiclient.ManifestRequest, creds git.Creds) ([]*unstructured.Unstructured, error) {
func runConfigManagementPlugin(appPath string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, creds git.Creds) ([]*unstructured.Unstructured, error) {
plugin := findPlugin(q.Plugins, q.ApplicationSource.Plugin.Name)
if plugin == nil {
return nil, fmt.Errorf("Config management plugin with name '%s' is not supported.", q.ApplicationSource.Plugin.Name)
}
env := append(os.Environ(), fmt.Sprintf("%s=%s", PluginEnvAppName, q.AppLabelValue), fmt.Sprintf("%s=%s", PluginEnvAppNamespace, q.Namespace))
env := append(os.Environ(), envVars.Environ()...)
if creds != nil {
closer, environ, err := creds.Environ()
if err != nil {

View file

@ -85,7 +85,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
assert.Equal(t, countOfManifests, len(res1.Manifests))
// this will test concatenated manifests to verify we split YAMLs correctly
res2, err := GenerateManifests("./testdata/concatenated", &q)
res2, err := GenerateManifests("./testdata/concatenated", "", &q)
assert.Nil(t, err)
assert.Equal(t, 3, len(res2.Manifests))
})
@ -296,9 +296,10 @@ func TestRunCustomTool(t *testing.T) {
func TestGenerateFromUTF16(t *testing.T) {
q := apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
ApplicationSource: &argoappv1.ApplicationSource{},
}
res1, err := GenerateManifests("./testdata/utf-16", &q)
res1, err := GenerateManifests("./testdata/utf-16", "", &q)
assert.Nil(t, err)
assert.Equal(t, 2, len(res1.Manifests))
}
@ -409,3 +410,22 @@ func TestGetRevisionMetadata(t *testing.T) {
assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags)
}
func Test_newEnv(t *testing.T) {
assert.Equal(t, &argoappv1.Env{
&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},
&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"},
&argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"},
&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"},
&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"},
&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"},
}, newEnv(&apiclient.ManifestRequest{
AppLabelValue: "my-app-name",
Namespace: "my-namespace",
Repo: &argoappv1.Repository{Repo: "https://github.com/my-org/my-repo"},
ApplicationSource: &argoappv1.ApplicationSource{
Path: "my-path",
TargetRevision: "my-target-revision",
},
}, "my-revision"))
}

View file

@ -4,7 +4,7 @@ import (
"fmt"
"io/ioutil"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/errors"
@ -87,8 +87,8 @@ func (a *Actions) CreateFromFile(handler func(app *Application)) *Actions {
}
}
if len(a.context.jsonnetTLAStr) > 0 || len(a.context.parameters) > 0 {
logrus.Fatal("Application parameters or json tlas are not supported")
if len(a.context.parameters) > 0 {
log.Fatal("Application parameters or json tlas are not supported")
}
if a.context.directoryRecurse {
@ -133,14 +133,6 @@ func (a *Actions) Create(args ...string) *Actions {
args = append(args, "--project", a.context.project)
for _, jsonnetTLAParameter := range a.context.jsonnetTLAStr {
args = append(args, "--jsonnet-tla-str", jsonnetTLAParameter)
}
for _, jsonnetTLAParameter := range a.context.jsonnetTLACode {
args = append(args, "--jsonnet-tla-code", jsonnetTLAParameter)
}
if a.context.namePrefix != "" {
args = append(args, "--nameprefix", a.context.namePrefix)
}
@ -157,6 +149,8 @@ func (a *Actions) Create(args ...string) *Actions {
args = append(args, "--revision", a.context.revision)
}
// are you adding new context values? if you only use them for this func, then use args instead
a.runCli(args...)
return a
@ -224,6 +218,8 @@ func (a *Actions) Sync(args ...string) *Actions {
args = append(args, "--force")
}
// are you adding new context values? if you only use them for this func, then use args instead
a.runCli(args...)
return a

View file

@ -24,8 +24,6 @@ type Context struct {
destServer string
env string
parameters []string
jsonnetTLAStr []string
jsonnetTLACode []string
namePrefix string
nameSuffix string
resource string
@ -171,16 +169,6 @@ func (c *Context) Parameter(parameter string) *Context {
return c
}
func (c *Context) JsonnetTLAStrParameter(parameter string) *Context {
c.jsonnetTLAStr = append(c.jsonnetTLAStr, parameter)
return c
}
func (c *Context) JsonnetTLACodeParameter(parameter string) *Context {
c.jsonnetTLACode = append(c.jsonnetTLACode, parameter)
return c
}
// group:kind:name
func (c *Context) SelectedResource(resource string) *Context {
c.resource = resource

View file

@ -148,10 +148,10 @@ func TestHelmSet(t *testing.T) {
Path("helm").
When().
Create().
AppSet("--helm-set", "foo=bar", "--helm-set", "foo=baz").
AppSet("--helm-set", "foo=bar", "--helm-set", "foo=baz", "--helm-set", "app=$ARGOCD_APP_NAME").
Then().
And(func(app *Application) {
assert.Equal(t, []HelmParameter{{Name: "foo", Value: "baz"}}, app.Spec.Source.Helm.Parameters)
assert.Equal(t, []HelmParameter{{Name: "foo", Value: "baz"}, {Name: "app", Value: "$ARGOCD_APP_NAME"}}, app.Spec.Source.Helm.Parameters)
})
}
@ -160,10 +160,41 @@ func TestHelmSetString(t *testing.T) {
Path("helm").
When().
Create().
AppSet("--helm-set-string", "foo=bar", "--helm-set-string", "foo=baz").
AppSet("--helm-set-string", "foo=bar", "--helm-set-string", "foo=baz", "--helm-set-string", "app=$ARGOCD_APP_NAME").
Then().
And(func(app *Application) {
assert.Equal(t, []HelmParameter{{Name: "foo", Value: "baz", ForceString: true}}, app.Spec.Source.Helm.Parameters)
assert.Equal(t, []HelmParameter{{Name: "foo", Value: "baz", ForceString: true}, {Name: "app", Value: "$ARGOCD_APP_NAME", ForceString: true}}, app.Spec.Source.Helm.Parameters)
})
}
// ensure we can use envsubst in "set" variables
func TestHelmSetEnv(t *testing.T) {
Given(t).
Path("helm-values").
When().
Create().
AppSet("--helm-set", "foo=$ARGOCD_APP_NAME").
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string))
})
}
func TestHelmSetStringEnv(t *testing.T) {
Given(t).
Path("helm-values").
When().
Create().
AppSet("--helm-set-string", "foo=$ARGOCD_APP_NAME").
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string))
})
}

View file

@ -5,8 +5,9 @@ import (
"github.com/stretchr/testify/assert"
. "github.com/argoproj/argo-cd/errors"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/test/e2e/fixture"
. "github.com/argoproj/argo-cd/test/e2e/fixture"
. "github.com/argoproj/argo-cd/test/e2e/fixture/app"
"github.com/argoproj/argo-cd/util/kube"
)
@ -20,7 +21,7 @@ func TestJsonnetAppliedCorrectly(t *testing.T) {
Then().
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
manifests, err := fixture.RunCli("app", "manifests", app.Name, "--source", "live")
manifests, err := RunCli("app", "manifests", app.Name, "--source", "live")
assert.NoError(t, err)
resources, err := kube.SplitYAML(manifests)
assert.NoError(t, err)
@ -44,15 +45,13 @@ func TestJsonnetAppliedCorrectly(t *testing.T) {
func TestJsonnetTlaParameterAppliedCorrectly(t *testing.T) {
Given(t).
Path("jsonnet-tla").
JsonnetTLAStrParameter("name=testing-tla").
JsonnetTLACodeParameter("replicas=3").
When().
Create().
Create("--jsonnet-tla-str", "name=testing-tla", "--jsonnet-tla-code", "replicas=0").
Sync().
Then().
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
manifests, err := fixture.RunCli("app", "manifests", app.Name, "--source", "live")
manifests, err := RunCli("app", "manifests", app.Name, "--source", "live")
assert.NoError(t, err)
resources, err := kube.SplitYAML(manifests)
assert.NoError(t, err)
@ -69,6 +68,35 @@ func TestJsonnetTlaParameterAppliedCorrectly(t *testing.T) {
deployment := resources[index]
assert.Equal(t, "testing-tla", deployment.GetName())
assert.Equal(t, int64(3), *kube.GetDeploymentReplicas(deployment))
assert.Equal(t, int64(0), *kube.GetDeploymentReplicas(deployment))
})
}
func TestJsonnetTlaEnv(t *testing.T) {
Given(t).
Path("jsonnet-tla-cm").
When().
Create("--jsonnet-tla-str", "foo=$ARGOCD_APP_NAME", "--jsonnet-tla-code", "bar='$ARGOCD_APP_NAME'").
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string))
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.bar}")).(string))
})
}
func TestJsonnetExtVarEnv(t *testing.T) {
Given(t).
Path("jsonnet-ext-var").
When().
Create("--jsonnet-ext-var-str", "foo=$ARGOCD_APP_NAME", "--jsonnet-ext-var-code", "bar='$ARGOCD_APP_NAME'").
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string))
assert.Equal(t, Name(), FailOnErr(Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "cm", "my-map", "-o", "jsonpath={.data.bar}")).(string))
})
}

View file

@ -0,0 +1,2 @@
version: 1.0.0
name: helm-values

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: my-map
data:
foo: {{.Values.foo}}

View file

@ -0,0 +1 @@
foo: bar

View file

@ -0,0 +1,11 @@
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: 'my-map',
},
data: {
foo: std.extVar('foo'),
bar: std.extVar('bar'),
}
}

View file

@ -0,0 +1,12 @@
function(foo='foo', bar='bar')
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: 'my-map',
},
data: {
foo: foo,
bar: bar,
}
}

View file

@ -4,10 +4,11 @@ import {FieldApi, FormApi, FormField as ReactFormField, Text, TextArea} from 're
import {ArrayInputField, CheckboxField, EditablePanel, EditablePanelItem, Expandable, TagsInputField} from '../../../shared/components';
import * as models from '../../../shared/models';
import {AuthSettings} from '../../../shared/models';
import {ApplicationSourceDirectory, AuthSettings} from '../../../shared/models';
import {services} from '../../../shared/services';
import {ImageTagFieldEditor} from './kustomize';
import * as kustomize from './kustomize-image';
import { VarsInputField } from './vars-input-field';
const TextWithMetadataField = ReactFormField((props: {metadata: { value: string }, fieldApi: FieldApi, className: string }) => {
const { fieldApi: {getValue, setValue}} = props;
@ -228,13 +229,28 @@ export const ApplicationParameters = (props: {
),
});
} else if (props.details.type === 'Directory') {
const directory = app.spec.source.directory || {} as ApplicationSourceDirectory;
attributes.push({
title: 'DIRECTORY RECURSE',
view: (!!(app.spec.source.directory && app.spec.source.directory.recurse)).toString(),
view: (!!directory.recurse).toString(),
edit: (formApi: FormApi) => (
<FormField formApi={formApi} field='spec.source.directory.recurse' component={CheckboxField}/>
),
});
attributes.push({
title: 'TOP-LEVEL ARGUMENTS',
view: (directory.jsonnet && directory.jsonnet.tlas || []).map((i, j) => <p key={j}>{i.name}='{i.value}' {i.code && 'code'}</p>),
edit: (formApi: FormApi) => (
<FormField field='spec.source.directory.jsonnet.tlas' formApi={formApi} component={VarsInputField}/>
),
});
attributes.push({
title: 'EXTERNAL VARIABLES',
view: (directory.jsonnet && directory.jsonnet.extVars || []).map((i, j) => <p key={j}>{i.name}='{i.value}' {i.code && 'code'}</p>),
edit: (formApi: FormApi) => (
<FormField field='spec.source.directory.jsonnet.extVars' formApi={formApi} component={VarsInputField}/>
),
});
}
return (

View file

@ -0,0 +1,26 @@
import { Checkbox } from 'argo-ui';
import * as React from 'react';
import * as ReactForm from 'react-form';
import { ArrayInput, hasNameAndValue, NameValueEditor } from '../../../shared/components';
export interface Var {
name: string;
value: string;
code: boolean;
}
const VarInputEditor = (item: Var, onChange: (item: Var) => any) => (
<React.Fragment>
{NameValueEditor(item, onChange)}
&nbsp;
<Checkbox checked={!!item.code} onChange={(val) => onChange({...item, code: val})} />
&nbsp;
</React.Fragment>
);
export const VarsInputField = ReactForm.FormField((props: { fieldApi: ReactForm.FieldApi }) => {
const {fieldApi: {getValue, setValue}} = props;
const val = getValue() || [];
return <ArrayInput editor={VarInputEditor} items={val} onChange={setValue} valid={hasNameAndValue}/>;
});

View file

@ -21,105 +21,90 @@ import * as ReactForm from 'react-form';
It does not allow re-ordering of elements (maybe in a v2).
*/
class Item {
public name: string;
public value: string;
export interface NameValue {
name: string;
value: string;
}
class Props {
public items: Item[];
public onChange: (items: Item[]) => void;
export const NameValueEditor = (item: NameValue, onChange: (item: NameValue) => any) => (
<React.Fragment>
<input placeholder='Name' value={item.name || ''} onChange={(e) => onChange({...item, name: e.target.value})} title='Name'/>
&nbsp;
=
&nbsp;
<input placeholder='Value' value={item.value || ''} onChange={(e) => onChange({...item, value: e.target.value})} title='Value'/>
&nbsp;
</React.Fragment>
);
interface Props<T> {
items: T[];
onChange: (items: T[]) => void;
editor: (item: T, onChange: (updated: T) => any) => React.ReactNode;
valid: (item: T) => boolean;
}
class State {
public items: Item[];
public newItem: Item;
}
export function ArrayInput<T>(props: Props<T>) {
const addItem = (item: T) => {
props.onChange([...props.items, item]);
};
export class ArrayInput extends React.Component<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
this.state = {newItem: {name: '', value: ''}, items: props.items};
}
const replaceItem = (item: T, i: number) => {
const items = props.items.slice();
items[i] = item;
props.onChange(items);
};
public render() {
const addItem = (i: Item) => {
this.setState((s) => {
s.items.push(i);
this.props.onChange(s.items);
return {items: s.items, newItem: {name: '', value: ''}};
});
};
const replaceItem = (i: Item, j: number) => {
this.setState((s) => {
s.items[j] = i;
this.props.onChange(s.items);
return s;
});
};
const removeItem = (j: number) => {
this.setState((s) => {
s.items.splice(j, 1);
this.props.onChange(s.items);
return s;
});
};
const setName = (name: string) => {
this.setState((s) => ({items: s.items, newItem: {name, value: s.newItem.value}}));
};
const setValue = (value: string) => {
this.setState((s) => ({items: s.items, newItem: {name: s.newItem.name, value}}));
};
return (
<div className='argo-field' style={{border: 0}}>
const removeItem = (i: number) => {
const items = props.items.slice();
items.splice(i, 1);
props.onChange(items);
};
const [newItem, setNewItem] = React.useState({} as T);
return (
<div className='argo-field' style={{border: 0}}>
<div>
{props.items.map((item, i) => (
<div key={`item-${i}`}>
{props.editor(item, (updated: T) => replaceItem(updated, i))}
&nbsp;
<button>
<i className='fa fa-times' style={{cursor: 'pointer'}} onClick={() => removeItem(i)}/>
</button>
</div>
))}
<div>
{this.state.items.map((i, j) => (
<div key={`item-${j}`}>
<input value={this.state.items[j].name}
onChange={(e) => replaceItem({name: e.target.value, value: i.value}, j)}/>
&nbsp;
=
&nbsp;
<input value={this.state.items[j].value}
onChange={(e) => replaceItem({name: i.name, value: e.target.value}, j)}/>
&nbsp;
<button >
<i className='fa fa-times' style={{cursor: 'pointer'}} onClick={() => removeItem(j)}/>
</button>
</div>
))}
</div>
<div>
<input placeholder='Name' value={this.state.newItem.name}
onChange={(e) => setName(e.target.value)}/>
{props.editor(newItem, setNewItem)}
&nbsp;
=
&nbsp;
<input placeholder='Value' value={this.state.newItem.value}
onChange={(e) => setValue(e.target.value)}/>
&nbsp;
<button disabled={this.state.newItem.name === '' || this.state.newItem.value === ''}
onClick={() => addItem(this.state.newItem)}>
<button disabled={!props.valid(newItem)} onClick={() => {
addItem(newItem);
setNewItem({} as T);
}}>
<i style={{cursor: 'pointer'}} className='fa fa-plus'/>
</button>
</div>
</div>
);
}
</div>
);
}
export function hasNameAndValue(item: {name?: string, value?: string}) {
return (item.name || '').trim() !== '' && (item.value || '').trim() !== '';
}
export const ArrayInputField = ReactForm.FormField((props: { fieldApi: ReactForm.FieldApi }) => {
const {fieldApi: {getValue, setValue}} = props;
return <ArrayInput items={getValue() || []} onChange={setValue}/>;
return <ArrayInput editor={NameValueEditor} items={getValue() || []} onChange={setValue} valid={hasNameAndValue} />;
});
export const MapInputField = ReactForm.FormField((props: { fieldApi: ReactForm.FieldApi }) => {
const {fieldApi: {getValue, setValue}} = props;
const items = new Array<Item>();
const items = new Array<NameValue>();
const map = getValue() || {};
Object.keys(map).forEach((key) => items.push({ name: key, value: map[key] }));
return (
<ArrayInput items={items} onChange={(array) => {
<ArrayInput editor={NameValueEditor} items={items} valid={hasNameAndValue} onChange={(array) => {
const newMap = {} as any;
array.forEach((item) => newMap[item.name] = item.value);
setValue(newMap);

View file

@ -176,18 +176,30 @@ export interface ApplicationSourceKsonnet {
parameters: KsonnetParameter[];
}
export interface PluginEnv {
export interface EnvEntry {
name: string;
value: string;
}
export interface ApplicationSourcePlugin {
name: string;
env: PluginEnv[];
env: EnvEntry[];
}
export interface JsonnetVar {
name: string;
value: string;
code: boolean;
}
interface ApplicationSourceJsonnet {
extVars: JsonnetVar[];
tlas: JsonnetVar[];
}
export interface ApplicationSourceDirectory {
recurse: boolean;
jsonnet?: ApplicationSourceJsonnet;
}
export interface SyncPolicy {
@ -478,7 +490,7 @@ export interface KustomizeAppSpec {
export interface PluginAppSpec {
name: string;
env: PluginEnv[];
env: EnvEntry[];
}
export interface ObjectReference {

23
white-list Normal file
View file

@ -0,0 +1,23 @@
# a list of sites we ignore when checking for broken links in mkdocs
10.97.164.88
192.168.0.20
argocd.example.com
api.github.com/user
cd.apps.argoproj.io
git.example.com
github.com/argoproj/another-private-repo
github.com/argoproj/my-private-repository
github.com/argoproj/other-private-repo
github.com/argoproj/private-repo
github.com/otherproj/another-private-repo
ksonnet.io
raw.githubusercontent.com/argoproj/argo-cd
repo.example.com
server.example.com
kubernetes.default.svc
kubernetes.default.svc:443
localhost:6443
localhost:8080
mycluster.com
storage.googleapis.com
your-kubernetes-cluster-addr