├── terraform-provider-concourse-128.png
├── .gitignore
├── CHANGELOG.md
├── integration
├── suite_test.go
├── setup_test.go
├── team_mgmt.go
└── pipeline_mgmt.go
├── main.go
├── .github
└── workflows
│ ├── keep-release-repo-deploy-key-active.yml
│ ├── sync-release-repo.yml
│ ├── lockdown-release-repo.yml
│ ├── ci.yml
│ └── release.yml
├── pkg
├── client
│ ├── transport.go
│ └── client.go
└── provider
│ ├── teams.go
│ ├── team_migrate_test.go
│ ├── config.go
│ ├── provider.go
│ ├── team_migrate.go
│ ├── util.go
│ ├── team.go
│ └── pipeline.go
├── docker-compose.yml
├── LICENSE
├── Makefile
├── README.md
├── docs
└── index.md
├── go.mod
├── terraform-provider-concourse.svg
└── go.sum
/terraform-provider-concourse-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alphagov/terraform-provider-concourse/HEAD/terraform-provider-concourse-128.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.tfstate
2 | *.tfstate.backup
3 | *.tfstate.backup*
4 | .terraform
5 | examples/terraform-provider-concourse
6 | terraform-provider-concourse
7 | keys/web/
8 | keys/worker/
9 | .idea/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | ### 8.0.0
4 |
5 | `concourse_pipeline` resource now supports supplying (concourse)
6 | template variables through the `vars` argument. Technically this is a
7 | breaking change if any of your pipelines happen to have any
8 | double-parentheses (`(( ... ))`) references that aren't intended to
9 | be interpreted by concourse.
10 |
--------------------------------------------------------------------------------
/integration/suite_test.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestIntegrationTests(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Integration tests")
13 | }
14 |
15 | var _ = BeforeSuite(SetupSuite)
16 | var _ = AfterSuite(TeardownSuite)
17 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 |
9 | "github.com/alphagov/terraform-provider-concourse/pkg/provider"
10 | )
11 |
12 | func main() {
13 | log.SetPrefix("[DEBUG] ")
14 |
15 | plugin.Serve(&plugin.ServeOpts{
16 | ProviderFunc: func() *schema.Provider {
17 | return provider.Provider()
18 | },
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/keep-release-repo-deploy-key-active.yml:
--------------------------------------------------------------------------------
1 | name: keep-release-repo-deploy-key-active
2 | on:
3 | schedule:
4 | - cron: '10 4 10 * *'
5 |
6 | jobs:
7 | clone-release-repo:
8 | if: ${{ github.repository_owner != 'terraform-provider-concourse' }}
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | repository: terraform-provider-concourse/terraform-provider-concourse
14 | ssh-key: ${{ secrets.RELEASE_REPO_SSH_PRIVATE_KEY }}
15 |
--------------------------------------------------------------------------------
/pkg/client/transport.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | // AuthenticatedTransport is a transport which adds the Authorization header
9 | type AuthenticatedTransport struct {
10 | AccessToken string
11 | TokenType string
12 | }
13 |
14 | // RoundTrip represents a single authorized request/response cycle
15 | func (t AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
16 | r.Header.Add(
17 | "Authorization",
18 | strings.Join([]string{t.TokenType, t.AccessToken}, " "),
19 | )
20 |
21 | return http.DefaultTransport.RoundTrip(r)
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/sync-release-repo.yml:
--------------------------------------------------------------------------------
1 | name: sync-release-repo
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | - 'sync-release-repo-test-*'
7 | branches:
8 | - master
9 | jobs:
10 | repo-sync:
11 | if: ${{ github.repository_owner == 'alphagov' }}
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: repo-sync
15 | uses: wei/git-sync@55c6b63b4f21607da0e9877ca9b4d11a29fc6d83 # v3.0.0
16 | with:
17 | source_repo: ${{ github.repository }}
18 | source_branch: ${{ github.ref }}
19 | destination_repo: "git@github.com:terraform-provider-concourse/terraform-provider-concourse.git"
20 | destination_branch: ${{ github.ref }}
21 | destination_ssh_private_key: ${{ secrets.RELEASE_REPO_SSH_PRIVATE_KEY }}
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | concourse-db:
5 | image: postgres
6 | environment:
7 | POSTGRES_DB: concourse
8 | POSTGRES_PASSWORD: concourse_pass
9 | POSTGRES_USER: concourse_user
10 | PGDATA: /database
11 |
12 | concourse:
13 | image: "concourse/concourse:${CONCOURSE_VERSION:-6.5.1}"
14 | command: web
15 | privileged: true
16 | depends_on: [concourse-db]
17 | ports: ["8080:8080"]
18 | volumes: ["./keys/web:/concourse-keys"]
19 | environment:
20 | CONCOURSE_POSTGRES_HOST: concourse-db
21 | CONCOURSE_POSTGRES_USER: concourse_user
22 | CONCOURSE_POSTGRES_PASSWORD: concourse_pass
23 | CONCOURSE_POSTGRES_DATABASE: concourse
24 | CONCOURSE_EXTERNAL_URL: http://localhost:8080
25 | CONCOURSE_ADD_LOCAL_USER: admin:password
26 | CONCOURSE_MAIN_TEAM_LOCAL_USER: admin
27 | CONCOURSE_WORKER_BAGGAGECLAIM_DRIVER: overlay
28 |
--------------------------------------------------------------------------------
/.github/workflows/lockdown-release-repo.yml:
--------------------------------------------------------------------------------
1 | name: lockdown-release-repo
2 | on:
3 | issues:
4 | types: opened
5 | pull_request_target:
6 | types: opened
7 |
8 | jobs:
9 | lockdown:
10 | if: ${{ github.repository_owner == 'terraform-provider-concourse' }}
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: dessant/repo-lockdown@0b093279a77b44bbc38e85089b5463dd06b4aea4 # v2.2.0
14 | with:
15 | github-token: ${{ github.token }}
16 | issue-comment: >
17 | Please submit issues at https://github.com/alphagov/terraform-provider-concourse,
18 | where development takes place. This repository is solely for the purpose of
19 | releases.
20 | pr-comment: >
21 | Please submit pull requests at https://github.com/alphagov/terraform-provider-concourse,
22 | where development takes place. This repository is solely for the purpose of
23 | releases.
24 |
--------------------------------------------------------------------------------
/pkg/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/concourse/concourse/go-concourse/concourse"
7 | "golang.org/x/oauth2"
8 | )
9 |
10 | // NewConcourseClient gives you an authenticated Concourse client using
11 | // local user username and password authentication. Separate from Basic Auth.
12 | func NewConcourseClient(
13 | url string,
14 | team string,
15 | username string, password string,
16 | ) (concourse.Client, error) {
17 |
18 | oauth2Config := oauth2.Config{
19 | ClientID: "fly",
20 | ClientSecret: "Zmx5",
21 |
22 | Endpoint: oauth2.Endpoint{TokenURL: url + "/sky/issuer/token"},
23 | Scopes: []string{"email", "federated:id", "groups", "openid", "profile"},
24 | }
25 |
26 | ctx := context.Background()
27 |
28 | tok, err := oauth2Config.PasswordCredentialsToken(ctx, username, password)
29 | if err != nil {
30 | return nil, err
31 | }
32 | tokenSource := oauth2.StaticTokenSource(tok)
33 | httpClient := oauth2.NewClient(ctx, tokenSource)
34 |
35 | return concourse.NewClient(url, httpClient, true), nil
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/provider/teams.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | )
9 |
10 | func dataTeams() *schema.Resource {
11 | return &schema.Resource{
12 | ReadContext: dataTeamsReads,
13 | Schema: map[string]*schema.Schema{
14 | "names": {
15 | Type: schema.TypeSet,
16 | Computed: true,
17 | Elem: &schema.Schema{Type: schema.TypeString},
18 | },
19 | },
20 | }
21 | }
22 |
23 | func dataTeamsReads(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
24 | client := m.(*ProviderConfig).Client
25 | teams, err := client.ListTeams()
26 | if err != nil {
27 | return diag.FromErr(err)
28 | }
29 |
30 | var names []string
31 |
32 | for _, team := range teams {
33 | names = append(names, team.Name)
34 | }
35 |
36 | d.SetId("concourse_teams")
37 | if err := d.Set("names", names); err != nil {
38 | return diag.Errorf("error setting team names: %s", err)
39 | }
40 |
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Crown Copyright (Government Digital Service)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/provider/team_migrate_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func getTeamStateDataV0() map[string]interface{} {
9 | return map[string]interface{}{
10 | "team_name": "foo123",
11 | "owners.#": "3",
12 | "owners.0": "bar",
13 | "owners.1": "baz",
14 | "owners.2": "qux",
15 | "pipeline_operators.#": "2",
16 | "pipeline_operators.0": "abc",
17 | "pipeline_operators.1": "def",
18 | }
19 | }
20 |
21 | func getTeamStateDataV1() map[string]interface{} {
22 | return map[string]interface{}{
23 | "team_name": "foo123",
24 | "owners.#": "3",
25 | "owners.1996459178": "bar",
26 | "owners.2015626392": "baz",
27 | "owners.2800005064": "qux",
28 | "pipeline_operators.#": "2",
29 | "pipeline_operators.891568578": "abc",
30 | "pipeline_operators.214229345": "def",
31 | }
32 | }
33 |
34 | func TestTeamStateUpgradeV0(t *testing.T) {
35 | expected := getTeamStateDataV1()
36 | actual, err := resourceTeamStateUpgradeV0(nil, getTeamStateDataV0(), nil)
37 |
38 | if err != nil {
39 | t.Fatalf("error migrating state: %s", err)
40 | }
41 |
42 | if !reflect.DeepEqual(expected, actual) {
43 | t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/provider/config.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/concourse/concourse/fly/rc"
7 | "github.com/concourse/concourse/go-concourse/concourse"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 |
10 | "github.com/alphagov/terraform-provider-concourse/pkg/client"
11 | )
12 |
13 | type ProviderConfig struct {
14 | Client concourse.Client
15 | }
16 |
17 | func ProviderConfigurationBuilder(
18 | d *schema.ResourceData,
19 | ) (interface{}, error) {
20 |
21 | targetName := rc.TargetName(d.Get("target").(string))
22 |
23 | if targetName != "" {
24 | target, err := rc.LoadTarget(targetName, false)
25 |
26 | if err != nil {
27 | return nil, fmt.Errorf("Error loading target: %s", err)
28 | }
29 |
30 | return &ProviderConfig{
31 | Client: target.Client(),
32 | }, nil
33 | }
34 |
35 | url := d.Get("url").(string)
36 | team := d.Get("team").(string)
37 | username := d.Get("username").(string)
38 | password := d.Get("password").(string)
39 |
40 | if url != "" && team != "" && username != "" && password != "" {
41 | c, err := client.NewConcourseClient(
42 | url,
43 | team,
44 | username, password,
45 | )
46 |
47 | if err != nil {
48 | return nil, fmt.Errorf("Error creating client: %s", err)
49 | }
50 |
51 | return &ProviderConfig{
52 | Client: c,
53 | }, nil
54 | }
55 |
56 | return nil, fmt.Errorf(
57 | `Please specify "target" or "username", "password", "team", and "url"`,
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | workflow_call:
6 |
7 | jobs:
8 |
9 | build:
10 | name: build
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | concourse-version:
15 | - "6.5.1"
16 | - "6.7.0"
17 | - "7.0.0"
18 | - "7.8.2"
19 | terraform-version:
20 | - "1.2"
21 | include:
22 | - terraform-version: "0.13"
23 | concourse-version: "7.8.2"
24 | - terraform-version: "0.14"
25 | concourse-version: "7.8.2"
26 | - terraform-version: "0.15"
27 | concourse-version: "7.8.2"
28 | - terraform-version: "1.0"
29 | concourse-version: "7.8.2"
30 | - terraform-version: "1.1"
31 | concourse-version: "7.8.2"
32 | steps:
33 | # avoid confusion around which terraform is being used
34 | - name: rm existing terraform
35 | run: type -P terraform && sudo rm $(type -P terraform)
36 |
37 | - name: setup
38 | uses: actions/setup-go@v2
39 | with:
40 | go-version: '1.18'
41 |
42 | - name: checkout
43 | uses: actions/checkout@v1
44 |
45 | - name: unit-tests
46 | run: make unit-tests
47 |
48 | - name: ensure-containers-exist
49 | env:
50 | CONCOURSE_VERSION: ${{ matrix.concourse-version }}
51 | run: |
52 | docker-compose up -d && docker-compose down
53 |
54 | - name: integration-tests
55 | env:
56 | CONCOURSE_VERSION: ${{ matrix.concourse-version }}
57 | TF_ACC_TERRAFORM_VERSION: ${{ matrix.terraform-version }}
58 | run: |
59 | sudo --preserve-env make integration-tests-prep-keys
60 | make integration-tests
61 |
--------------------------------------------------------------------------------
/pkg/provider/provider.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
5 | )
6 |
7 | func Provider() *schema.Provider {
8 | return &schema.Provider{
9 | Schema: map[string]*schema.Schema{
10 | "target": {
11 | Type: schema.TypeString,
12 | DefaultFunc: schema.EnvDefaultFunc("FLY_TARGET", nil),
13 | Description: "Target as in 'fly --target', do not use if using team/username/password",
14 | Optional: true,
15 | },
16 | "url": {
17 | Type: schema.TypeString,
18 | DefaultFunc: schema.EnvDefaultFunc("FLY_URL", nil),
19 | Description: "URL, do not use if using target ",
20 | Optional: true,
21 | },
22 | "team": {
23 | Type: schema.TypeString,
24 | DefaultFunc: schema.EnvDefaultFunc("FLY_TEAM", nil),
25 | Description: "Team name, do not use if using target ",
26 | Optional: true,
27 | },
28 | "username": {
29 | Type: schema.TypeString,
30 | DefaultFunc: schema.EnvDefaultFunc("FLY_USERNAME", nil),
31 | Description: "Username, do not use if using target",
32 | Optional: true,
33 | },
34 | "password": {
35 | Type: schema.TypeString,
36 | DefaultFunc: schema.EnvDefaultFunc("FLY_PASSWORD", nil),
37 | Description: "Password, do not use if using target ",
38 | Optional: true,
39 | },
40 | },
41 |
42 | ConfigureFunc: ProviderConfigurationBuilder,
43 |
44 | DataSourcesMap: map[string]*schema.Resource{
45 | "concourse_pipeline": dataPipeline(),
46 | "concourse_team": dataTeam(),
47 | "concourse_teams": dataTeams(),
48 | },
49 |
50 | ResourcesMap: map[string]*schema.Resource{
51 | "concourse_pipeline": resourcePipeline(),
52 | "concourse_team": resourceTeam(),
53 | },
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | OS = $$(go env GOOS)
3 | ARCH = $$(go env GOARCH)
4 |
5 | GENERATE_KEY := \
6 | docker run --rm -v $$PWD/keys:/keys --user $$(id -u):$$(id -g) \
7 | concourse/concourse:$${CONCOURSE_VERSION:-6.5.1} \
8 | generate-key
9 |
10 | # shouldn't use CGO on binaries produced for the terraform registry
11 | export CGO_ENABLED=0
12 |
13 | .PHONY: build
14 | build: clean terraform-provider-concourse
15 |
16 | .PHONY: clean
17 | clean:
18 | go clean
19 |
20 | terraform-provider-concourse:
21 | go build -o terraform-provider-concourse
22 |
23 | .PHONY: install
24 | install: terraform-provider-concourse
25 | @mkdir -p ~/.terraform.d/plugins/$(OS)_$(ARCH)
26 | @cp terraform-provider-concourse ~/.terraform.d/plugins/$(OS)_$(ARCH)
27 | @echo Installed terraform provider into ~/.terraform.d/plugins/$(OS)_$(ARCH)
28 |
29 | keys/web/session_signing_key:
30 | mkdir -p keys/web
31 | $(GENERATE_KEY) -t rsa -f /$@
32 |
33 | keys/web/tsa_host_key:
34 | mkdir -p keys/web
35 | $(GENERATE_KEY) -t ssh -f/$@
36 |
37 | keys/worker/worker_key:
38 | mkdir -p keys/worker
39 | $(GENERATE_KEY) -t ssh -f /$@
40 |
41 | keys/worker/tsa_host_key.pub: keys/web/tsa_host_key
42 | mkdir -p keys/worker
43 | cp keys/web/tsa_host_key.pub $@
44 |
45 | keys/web/authorized_worker_keys: keys/worker/worker_key
46 | mkdir -p keys/web
47 | cp keys/worker/worker_key.pub $@
48 |
49 | # separate from `integration-tests` so it can be run as root without
50 | # running the whole integration tests as root
51 | .PHONY: integration-tests-prep-keys
52 | integration-tests-prep-keys: keys/web/session_signing_key keys/web/tsa_host_key keys/worker/worker_key keys/worker/tsa_host_key.pub keys/web/authorized_worker_keys
53 |
54 | .PHONY: integration-tests
55 | integration-tests: integration-tests-prep-keys
56 | go test -count 1 -v ./integration
57 |
58 | .PHONY: unit-tests
59 | unit-tests:
60 | go test -count 1 -v ./pkg/provider
61 |
--------------------------------------------------------------------------------
/pkg/provider/team_migrate.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func resourceTeamResourceV0() *schema.Resource {
12 | return &schema.Resource{
13 | Schema: map[string]*schema.Schema{
14 |
15 | "team_name": &schema.Schema{
16 | Type: schema.TypeString,
17 | Required: true,
18 | },
19 |
20 | "owners": &schema.Schema{
21 | Type: schema.TypeList,
22 | Required: true,
23 | DefaultFunc: func() (interface{}, error) {
24 | return make([]string, 0), nil
25 | },
26 | Elem: &schema.Schema{
27 | Type: schema.TypeString,
28 | },
29 | },
30 |
31 | "members": &schema.Schema{
32 | Type: schema.TypeList,
33 | Optional: true,
34 | DefaultFunc: func() (interface{}, error) {
35 | return make([]string, 0), nil
36 | },
37 | Elem: &schema.Schema{
38 | Type: schema.TypeString,
39 | },
40 | },
41 |
42 | "pipeline_operators": &schema.Schema{
43 | Type: schema.TypeList,
44 | Optional: true,
45 | DefaultFunc: func() (interface{}, error) {
46 | return make([]string, 0), nil
47 | },
48 | Elem: &schema.Schema{
49 | Type: schema.TypeString,
50 | },
51 | },
52 |
53 | "viewers": &schema.Schema{
54 | Type: schema.TypeList,
55 | Optional: true,
56 | DefaultFunc: func() (interface{}, error) {
57 | return make([]string, 0), nil
58 | },
59 | Elem: &schema.Schema{
60 | Type: schema.TypeString,
61 | },
62 | },
63 | },
64 | }
65 | }
66 |
67 | func resourceTeamStateUpgradeV0(
68 | _ context.Context,
69 | rawState map[string]interface{},
70 | meta interface{},
71 | ) (map[string]interface{}, error) {
72 | isNotDigit := func(c rune) bool { return c < '0' || c > '9' }
73 |
74 | rawStateOut := map[string]interface{}{}
75 |
76 | for k, v := range rawState {
77 | splitKey := strings.Split(k, ".")
78 | if len(splitKey) == 2 && strings.IndexFunc(splitKey[1], isNotDigit) == -1 {
79 | switch splitKey[0] {
80 | case
81 | "owners",
82 | "members",
83 | "pipeline_operators",
84 | "viewers":
85 | rawStateOut[fmt.Sprintf("%s.%d", splitKey[0], schema.HashString(v))] = v
86 | continue
87 | }
88 | }
89 | rawStateOut[k] = v
90 | }
91 | return rawStateOut, nil
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/provider/util.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/concourse/concourse/go-concourse/concourse"
6 | "github.com/concourse/concourse/vars"
7 | "github.com/ghodss/yaml"
8 | "strings"
9 | )
10 |
11 | // JSONToJSON ensures that keys are ordered, etc, by double converting
12 | func JSONToJSON(inputJSON string) (string, error) {
13 | intermediateYAML, err := yaml.JSONToYAML([]byte(inputJSON))
14 |
15 | if err != nil {
16 | return "", err
17 | }
18 |
19 | outputJSON, err := yaml.YAMLToJSON(intermediateYAML)
20 |
21 | if err != nil {
22 | return "", err
23 | }
24 |
25 | return string(outputJSON), nil
26 | }
27 |
28 | // YAMLToJSON is just a wrapper for less type boilerplate
29 | func YAMLToJSON(inputYAML string) (string, error) {
30 | outputJSON, err := yaml.YAMLToJSON([]byte(inputYAML))
31 |
32 | if err != nil {
33 | return "", err
34 | }
35 |
36 | return string(outputJSON), nil
37 | }
38 |
39 | // JSONToYAML is just a wrapper for less type boilerplate
40 | func JSONToYAML(inputJSON string) (string, error) {
41 | outputYAML, err := yaml.JSONToYAML([]byte(inputJSON))
42 |
43 | if err != nil {
44 | return "", err
45 | }
46 |
47 | return string(outputYAML), nil
48 | }
49 |
50 | // ParsePipelineConfig returns parsed/validated JSON
51 | // from either YAML or JSON
52 | func ParsePipelineConfig(
53 | pipelineConfig string,
54 | pipelineConfigFormat string,
55 | inputVars map[string]interface{},
56 | ) (string, error) {
57 | var err error
58 | outputJSON := ""
59 |
60 | if inputVars != nil {
61 | params := []vars.Variables{vars.StaticVariables(inputVars)}
62 | evaluatedConfig, err := vars.NewTemplateResolver([]byte(pipelineConfig), params).Resolve(false, false)
63 | if err != nil {
64 | return "", err
65 | }
66 |
67 | pipelineConfig = string(evaluatedConfig[:])
68 | }
69 |
70 | if pipelineConfigFormat == "json" {
71 | outputJSON, err = JSONToJSON(pipelineConfig)
72 | if err != nil {
73 | return "", err
74 | }
75 | }
76 |
77 | if pipelineConfigFormat == "yaml" {
78 | outputJSON, err = YAMLToJSON(pipelineConfig)
79 | if err != nil {
80 | return "", err
81 | }
82 | }
83 |
84 | return outputJSON, nil
85 | }
86 |
87 | func SerializeWarnings(warnings []concourse.ConfigWarning) string {
88 | var warningsMsg strings.Builder
89 | if len(warnings) > 0 {
90 | warningsMsg.WriteString(fmt.Sprintln())
91 | for _, warning := range warnings {
92 | warningsMsg.WriteString(fmt.Sprintf(" - %v\n", warning.Message))
93 | }
94 | }
95 |
96 | return warningsMsg.String()
97 | }
98 |
--------------------------------------------------------------------------------
/integration/setup_test.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 |
8 | "github.com/concourse/concourse/go-concourse/concourse"
9 |
10 | "github.com/alphagov/terraform-provider-concourse/pkg/client"
11 |
12 | . "github.com/onsi/ginkgo"
13 | . "github.com/onsi/gomega"
14 | "github.com/onsi/gomega/gexec"
15 | )
16 |
17 | const (
18 | concourseURL = "http://localhost:8080"
19 | concourseTeam = "main"
20 | concourseUsername = "admin"
21 | concoursePassword = "password"
22 | )
23 |
24 | func NewConcourseClient() (concourse.Client, error) {
25 | c, err := client.NewConcourseClient(
26 | concourseURL,
27 | concourseTeam,
28 | concourseUsername, concoursePassword,
29 | )
30 |
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | return c, nil
36 | }
37 |
38 | func SetupSuite() {
39 | Expect(os.Setenv("TF_ACC", "true")).NotTo(HaveOccurred())
40 |
41 | Expect(os.Setenv("FLY_URL", concourseURL)).NotTo(HaveOccurred())
42 | Expect(os.Setenv("FLY_TEAM", concourseTeam)).NotTo(HaveOccurred())
43 | Expect(os.Setenv("FLY_USERNAME", concourseUsername)).NotTo(HaveOccurred())
44 | Expect(os.Setenv("FLY_PASSWORD", concoursePassword)).NotTo(HaveOccurred())
45 |
46 | buildCmd := exec.Command("docker-compose", "build")
47 | session, err := gexec.Start(buildCmd, GinkgoWriter, GinkgoWriter)
48 | Expect(err).ShouldNot(HaveOccurred())
49 | Eventually(session, 300).Should(gexec.Exit(0))
50 | }
51 |
52 | func SetupTest() {
53 | upCmd := exec.Command("docker-compose", "up", "-d", "--force-recreate")
54 | session, err := gexec.Start(upCmd, GinkgoWriter, GinkgoWriter)
55 | Expect(err).ShouldNot(HaveOccurred())
56 | Eventually(session, 120).Should(gexec.Exit(0))
57 |
58 | Eventually(func() error {
59 | fmt.Println("Waiting for Concourse to be ready")
60 |
61 | client, err := NewConcourseClient()
62 |
63 | if err != nil {
64 | return err
65 | }
66 |
67 | _, err = client.ListWorkers()
68 |
69 | if err != nil {
70 | return err
71 | }
72 |
73 | fmt.Println("Concourse is ready")
74 | return nil
75 | }, "180s", "3s").ShouldNot(HaveOccurred())
76 | }
77 |
78 | func TeardownTest() {
79 | downCmd := exec.Command("docker-compose", "down")
80 | session, err := gexec.Start(downCmd, GinkgoWriter, GinkgoWriter)
81 | Expect(err).ShouldNot(HaveOccurred())
82 | Eventually(session, 60).Should(gexec.Exit(0))
83 | }
84 |
85 | func TeardownSuite() {
86 | gexec.KillAndWait()
87 | }
88 |
89 | type GinkgoTerraformTestingT struct {
90 | GinkgoTInterface
91 |
92 | CurrentDescription GinkgoTestDescription
93 | }
94 |
95 | func NewGinkoTerraformTestingT() GinkgoTerraformTestingT {
96 | return GinkgoTerraformTestingT{GinkgoT(), CurrentGinkgoTestDescription()}
97 | }
98 |
99 | func (t GinkgoTerraformTestingT) Helper() {}
100 |
101 | func (t GinkgoTerraformTestingT) Name() string {
102 | return t.CurrentDescription.FullTestText
103 | }
104 |
105 | func (t GinkgoTerraformTestingT) Verbose() bool {
106 | return true
107 | }
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # terraform-provider-concourse
2 |
3 | ## What
4 |
5 | A terraform provider for concourse
6 |
7 | ## Why
8 |
9 | `fly` is an amazing tool, but configuration using scripts running fly is not
10 | ideal.
11 |
12 | ## Prerequisites
13 |
14 | Install `go`, and `terraform`.
15 |
16 | ## How to install and use
17 |
18 | ```
19 | make install
20 | ```
21 |
22 | ## How to build and test for development
23 |
24 | ```
25 | make
26 | make integration-tests
27 | ```
28 |
29 | # Example `terraform`
30 |
31 | ## Create a provider (using target from fly)
32 |
33 | ```hcl
34 | provider "concourse" {
35 | target = "target_name"
36 | }
37 | ```
38 |
39 | ## Create a provider (using a local username and password)
40 |
41 | Note: this is not basic authentication
42 |
43 | ```hcl
44 | provider "concourse" {
45 | url = "https://wings.pivotal.io"
46 | team = "main"
47 |
48 | username = "localuser"
49 | password = "very-secure-password"
50 | }
51 | ```
52 |
53 | ## Look up all teams
54 |
55 | ```hcl
56 | data "concourse_teams" "teams" {
57 | }
58 |
59 | output "team_names" {
60 | value = data.concourse_teams.teams.names
61 | }
62 | ```
63 |
64 | ## Look up a team
65 |
66 | ```hcl
67 | data "concourse_team" "my_team" {
68 | team_name = "main"
69 | }
70 |
71 | output "my_team_name" {
72 | value = data.concourse_team.my_team.team_name
73 | }
74 |
75 | output "my_team_owners" {
76 | value = data.concourse_team.my_team.owners
77 | }
78 |
79 | output "my_team_members" {
80 | value = data.concourse_team.my_team.members
81 | }
82 |
83 | output "my_team_pipeline_operators" {
84 | value = data.concourse_team.my_team.pipeline_operators
85 | }
86 |
87 | output "my_team_viewers" {
88 | value = data.concourse_team.my_team.viewers
89 | }
90 | ```
91 |
92 | ## Look up a pipeline
93 |
94 | ```hcl
95 | data "concourse_pipeline" "my_pipeline" {
96 | team_name = "main"
97 | pipeline_name = "pipeline"
98 | }
99 |
100 | output "my_pipeline_team_name" {
101 | value = data.concourse_pipeline.my_pipeline.team_name
102 | }
103 |
104 | output "my_pipeline_pipeline_name" {
105 | value = data.concourse_pipeline.my_pipeline.pipeline_name
106 | }
107 |
108 | output "my_pipeline_is_exposed" {
109 | value = data.concourse_pipeline.my_pipeline.is_exposed
110 | }
111 |
112 | output "my_pipeline_is_paused" {
113 | value = data.concourse_pipeline.my_pipeline.is_paused
114 | }
115 |
116 | output "my_pipeline_json" {
117 | value = data.concourse_pipeline.my_pipeline.json
118 | }
119 |
120 | output "my_pipeline_yaml" {
121 | value = data.concourse_pipeline.my_pipeline.yaml
122 | }
123 | ```
124 | ## Create a team
125 |
126 | Supports `owners`, `members`, `pipeline_operators`, and `viewers`.
127 |
128 | Specify users and groups by prefixing the strings:
129 |
130 | * `user:`
131 | * `group:`
132 |
133 | ```hcl
134 | resource "concourse_team" "my_team" {
135 | team_name = "my-team"
136 |
137 | owners = [
138 | "group:github:org-name",
139 | "group:github:org-name:team-name",
140 | "user:github:tlwr",
141 | ]
142 |
143 | viewers = [
144 | "user:github:samrees"
145 | ]
146 | }
147 | ```
148 |
149 | ## Create a pipeline
150 |
151 | ```hcl
152 | resource "concourse_pipeline" "my_pipeline" {
153 | team_name = "main"
154 | pipeline_name = "my-pipeline"
155 |
156 | is_exposed = true
157 | is_paused = true
158 |
159 | pipeline_config = file("pipeline-config.yml")
160 | pipeline_config_format = "yaml"
161 | }
162 |
163 | # OR
164 |
165 | resource "concourse_pipeline" "my_pipeline" {
166 | team_name = "main"
167 | pipeline_name = "my-pipeline"
168 |
169 | is_exposed = true
170 | is_paused = true
171 |
172 | pipeline_config = file("pipeline-config.json")
173 | pipeline_config_format = "json"
174 | }
175 | ```
176 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Concourse Provider
2 |
3 | A terraform provider for concourse
4 |
5 | ## Why
6 |
7 | `fly` is an amazing tool, but configuration using scripts running fly is not
8 | ideal.
9 |
10 | ## Example Usage
11 |
12 | ### Create a provider (using target from fly)
13 |
14 | ```hcl
15 | provider "concourse" {
16 | target = "target_name"
17 | }
18 | ```
19 |
20 | ### Create a provider (using a local username and password)
21 |
22 | Note: this is not basic authentication
23 |
24 | ```hcl
25 | provider "concourse" {
26 | url = "https://wings.pivotal.io"
27 | team = "main"
28 |
29 | username = "localuser"
30 | password = "very-secure-password"
31 | }
32 | ```
33 |
34 | ### Look up all teams
35 |
36 | ```hcl
37 | data "concourse_teams" "teams" {
38 | }
39 |
40 | output "team_names" {
41 | value = data.concourse_teams.teams.names
42 | }
43 | ```
44 |
45 | ### Look up a team
46 |
47 | ```hcl
48 | data "concourse_team" "my_team" {
49 | team_name = "main"
50 | }
51 |
52 | output "my_team_name" {
53 | value = data.concourse_team.my_team.team_name
54 | }
55 |
56 | output "my_team_owners" {
57 | value = data.concourse_team.my_team.owners
58 | }
59 |
60 | output "my_team_members" {
61 | value = data.concourse_team.my_team.members
62 | }
63 |
64 | output "my_team_pipeline_operators" {
65 | value = data.concourse_team.my_team.pipeline_operators
66 | }
67 |
68 | output "my_team_viewers" {
69 | value = data.concourse_team.my_team.viewers
70 | }
71 | ```
72 |
73 | ### Look up a pipeline
74 |
75 | ```hcl
76 | data "concourse_pipeline" "my_pipeline" {
77 | team_name = "main"
78 | pipeline_name = "pipeline"
79 | }
80 |
81 | output "my_pipeline_team_name" {
82 | value = data.concourse_pipeline.my_pipeline.team_name
83 | }
84 |
85 | output "my_pipeline_pipeline_name" {
86 | value = data.concourse_pipeline.my_pipeline.pipeline_name
87 | }
88 |
89 | output "my_pipeline_is_exposed" {
90 | value = data.concourse_pipeline.my_pipeline.is_exposed
91 | }
92 |
93 | output "my_pipeline_is_paused" {
94 | value = data.concourse_pipeline.my_pipeline.is_paused
95 | }
96 |
97 | output "my_pipeline_json" {
98 | value = data.concourse_pipeline.my_pipeline.json
99 | }
100 |
101 | output "my_pipeline_yaml" {
102 | value = data.concourse_pipeline.my_pipeline.yaml
103 | }
104 | ```
105 |
106 | ### Create a team
107 |
108 | Supports `owners`, `members`, `pipeline_operators`, and `viewers`.
109 |
110 | Specify users and groups by prefixing the strings:
111 |
112 | * `user:`
113 | * `group:`
114 |
115 | ```hcl
116 | resource "concourse_team" "my_team" {
117 | team_name = "my-team"
118 |
119 | owners = [
120 | "group:github:org-name",
121 | "group:github:org-name:team-name",
122 | "user:github:tlwr",
123 | ]
124 |
125 | viewers = [
126 | "user:github:samrees"
127 | ]
128 | }
129 | ```
130 |
131 | ### Create a pipeline
132 |
133 | ```hcl
134 | resource "concourse_pipeline" "my_pipeline" {
135 | team_name = "main"
136 | pipeline_name = "my-pipeline"
137 |
138 | is_exposed = true
139 | is_paused = true
140 |
141 | pipeline_config = file("pipeline-config.yml")
142 | pipeline_config_format = "yaml"
143 |
144 | vars = {
145 | foo = "bar"
146 | }
147 | }
148 |
149 | # OR
150 |
151 | resource "concourse_pipeline" "my_pipeline" {
152 | team_name = "main"
153 | pipeline_name = "my-pipeline"
154 |
155 | is_exposed = true
156 | is_paused = true
157 |
158 | pipeline_config = file("pipeline-config.json")
159 | pipeline_config_format = "json"
160 |
161 | vars = {
162 | foo = "bar"
163 | }
164 | }
165 | ```
166 |
167 | ## Import
168 |
169 | Concourse teams can be imported using the team name e.g.
170 |
171 | ```
172 | $ terraform import concourse_pipeline.my_team my-team
173 | ```
174 |
175 | Concourse pipelines can be imported using the team name and pipeline name e.g.
176 |
177 | ```
178 | $ terraform import concourse_pipeline.my_app my-team:my-app
179 | ```
180 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alphagov/terraform-provider-concourse
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/concourse/concourse v1.6.1-0.20200820185530-cfe7746ae742
7 | github.com/ghodss/yaml v1.0.0
8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.21.0
9 | github.com/onsi/ginkgo v1.16.5
10 | github.com/onsi/gomega v1.20.0
11 | golang.org/x/oauth2 v0.7.0
12 | )
13 |
14 | require (
15 | github.com/agext/levenshtein v1.2.2 // indirect
16 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
17 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a // indirect
18 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
19 | github.com/cppforlife/go-semi-semantic v0.0.0-20160921010311-576b6af77ae4 // indirect
20 | github.com/davecgh/go-spew v1.1.1 // indirect
21 | github.com/fatih/color v1.13.0 // indirect
22 | github.com/fsnotify/fsnotify v1.4.9 // indirect
23 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
24 | github.com/golang/protobuf v1.5.3 // indirect
25 | github.com/google/go-cmp v0.5.9 // indirect
26 | github.com/google/jsonapi v0.0.0-20180618021926-5d047c6bc66b // indirect
27 | github.com/hashicorp/errwrap v1.0.0 // indirect
28 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect
29 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
30 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
31 | github.com/hashicorp/go-hclog v1.2.1 // indirect
32 | github.com/hashicorp/go-multierror v1.1.1 // indirect
33 | github.com/hashicorp/go-plugin v1.4.4 // indirect
34 | github.com/hashicorp/go-uuid v1.0.3 // indirect
35 | github.com/hashicorp/go-version v1.6.0 // indirect
36 | github.com/hashicorp/hc-install v0.4.0 // indirect
37 | github.com/hashicorp/hcl/v2 v2.13.0 // indirect
38 | github.com/hashicorp/logutils v1.0.0 // indirect
39 | github.com/hashicorp/terraform-exec v0.17.2 // indirect
40 | github.com/hashicorp/terraform-json v0.14.0 // indirect
41 | github.com/hashicorp/terraform-plugin-go v0.14.0 // indirect
42 | github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
43 | github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect
44 | github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
45 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
46 | github.com/jessevdk/go-flags v1.5.0 // indirect
47 | github.com/mattn/go-colorable v0.1.12 // indirect
48 | github.com/mattn/go-isatty v0.0.14 // indirect
49 | github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3 // indirect
50 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
51 | github.com/mitchellh/copystructure v1.2.0 // indirect
52 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
53 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect
54 | github.com/mitchellh/mapstructure v1.5.0 // indirect
55 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
56 | github.com/nxadm/tail v1.4.8 // indirect
57 | github.com/oklog/run v1.0.0 // indirect
58 | github.com/peterhellberg/link v1.0.0 // indirect
59 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958 // indirect
60 | github.com/vito/go-sse v0.0.0-20160212001227-fd69d275caac // indirect
61 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
62 | github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
63 | github.com/vmihailenco/tagparser v0.1.1 // indirect
64 | github.com/zclconf/go-cty v1.10.0 // indirect
65 | golang.org/x/crypto v0.21.0 // indirect
66 | golang.org/x/mod v0.8.0 // indirect
67 | golang.org/x/net v0.23.0 // indirect
68 | golang.org/x/sys v0.18.0 // indirect
69 | golang.org/x/text v0.14.0 // indirect
70 | golang.org/x/tools v0.6.0 // indirect
71 | google.golang.org/api v0.110.0 // indirect
72 | google.golang.org/appengine v1.6.7 // indirect
73 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
74 | google.golang.org/grpc v1.56.3 // indirect
75 | google.golang.org/protobuf v1.33.0 // indirect
76 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
77 | gopkg.in/yaml.v2 v2.3.0 // indirect
78 | gopkg.in/yaml.v3 v3.0.1 // indirect
79 | sigs.k8s.io/yaml v1.1.0 // indirect
80 | )
81 |
--------------------------------------------------------------------------------
/terraform-provider-concourse.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
100 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - 'v*.*'
6 | - 'release-test-*'
7 |
8 | jobs:
9 |
10 | test:
11 | if: ${{ github.repository_owner == 'terraform-provider-concourse' }}
12 | uses: ./.github/workflows/ci.yml
13 |
14 | get-tag-name:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - id: get-tag-name-step
18 | run: |
19 | export TAG_NAME="${GITHUB_REF/refs\/tags\//}"
20 | export RAW_VERSION="${TAG_NAME##v}"
21 | echo "::set-output name=tag_name::${TAG_NAME}"
22 | echo "::set-output name=raw_version::${RAW_VERSION}"
23 | outputs:
24 | tag_name: ${{ steps.get-tag-name-step.outputs.tag_name }}
25 | raw_version: ${{ steps.get-tag-name-step.outputs.raw_version }}
26 |
27 | build:
28 | runs-on: ubuntu-latest
29 | needs: [ "test", "get-tag-name" ]
30 | strategy:
31 | matrix:
32 | goos: [ "linux", "darwin" ]
33 | goarch: [ "amd64", "arm64" ]
34 | steps:
35 | - name: setup
36 | uses: actions/setup-go@v2
37 | with:
38 | go-version: '1.18'
39 |
40 | - name: checkout
41 | uses: actions/checkout@v1
42 |
43 | - name: compile
44 | id: compile
45 | env:
46 | BINARY_NAME: terraform-provider-concourse_${{ needs.get-tag-name.outputs.raw_version }}
47 | ZIP_NAME: terraform-provider-concourse_${{ needs.get-tag-name.outputs.raw_version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip
48 | run: |
49 | GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" make
50 | mv terraform-provider-concourse $BINARY_NAME
51 | zip $ZIP_NAME $BINARY_NAME
52 | echo "::set-output name=zip_name::$ZIP_NAME"
53 |
54 | - id: upload-artifact
55 | uses: actions/upload-artifact@v2
56 | with:
57 | name: ${{ steps.compile.outputs.zip_name }}
58 | path: ./${{ steps.compile.outputs.zip_name }}
59 |
60 | sign:
61 | runs-on: ubuntu-latest
62 | needs: [ "build", "get-tag-name" ]
63 | steps:
64 | - name: import-gpg-key
65 | id: import-gpg-key
66 | uses: paultyng/ghaction-import-gpg@53deb67fe3b05af114ad9488a4da7b782455d588 # v2.1.0
67 | env:
68 | GPG_PRIVATE_KEY: ${{ secrets.GPG_SECRET_KEY }}
69 | PASSPHRASE: ${{ secrets.GPG_SECRET_KEY_PASSPHRASE }}
70 | - name: download-artifacts
71 | id: download-artifacts
72 | uses: actions/download-artifact@v2
73 | with:
74 | path: artifacts/
75 | - name: gather-hash-sign
76 | id: gather-hash-sign
77 | env:
78 | HASH_FILE_NAME: terraform-provider-concourse_${{ needs.get-tag-name.outputs.raw_version }}_SHA256SUMS
79 | run: |
80 | mkdir gathered
81 | cp artifacts/*.zip/*.zip gathered/
82 | pushd gathered
83 | sha256sum *.zip > $HASH_FILE_NAME
84 | gpg --batch --local-user E2CE34DCDC76573D80BC35533BA5353D6C041A26 --detach-sign $HASH_FILE_NAME
85 | popd
86 | find gathered/ -type f -printf '%f\n' | jq --raw-input -c --slurp '.[:-1] | split("\n")' > all_artifacts.json
87 | echo "::set-output name=hash_file_name::$HASH_FILE_NAME"
88 | echo -n "::set-output name=all_artifacts_json::"
89 | cat all_artifacts.json
90 | - name: upload-hash-file-artifact
91 | id: upload-hash-file-artifact
92 | uses: actions/upload-artifact@v2
93 | with:
94 | name: ${{ steps.gather-hash-sign.outputs.hash_file_name }}
95 | path: ./gathered/${{ steps.gather-hash-sign.outputs.hash_file_name }}
96 | - name: upload-signature-artifact
97 | id: upload-signature-artifact
98 | uses: actions/upload-artifact@v2
99 | with:
100 | name: ${{ steps.gather-hash-sign.outputs.hash_file_name }}.sig
101 | path: ./gathered/${{ steps.gather-hash-sign.outputs.hash_file_name }}.sig
102 | outputs:
103 | all_artifacts_json: ${{ steps.gather-hash-sign.outputs.all_artifacts_json }}
104 |
105 | create-release:
106 | runs-on: ubuntu-latest
107 | needs: build
108 | steps:
109 | - name: create-release
110 | id: create-release
111 | uses: actions/create-release@v1
112 | env:
113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114 | with:
115 | tag_name: ${{ github.ref }}
116 | release_name: ${{ github.ref }}
117 | outputs:
118 | release_upload_url: ${{ steps.create-release.outputs.upload_url }}
119 |
120 | upload-release-assets:
121 | runs-on: ubuntu-latest
122 | needs: [ "create-release", "sign" ]
123 | strategy:
124 | matrix:
125 | artifact: ${{ fromJson(needs.sign.outputs.all_artifacts_json) }}
126 | steps:
127 | - name: download-artifact
128 | id: download-artifact
129 | uses: actions/download-artifact@v2
130 | with:
131 | name: ${{ matrix.artifact }}
132 |
133 | - name: determine-content-type
134 | id: determine-content-type
135 | env:
136 | ARTIFACT_NAME: ${{ matrix.artifact }}
137 | run: |
138 | echo -n '::set-output name=content_type::'
139 | if [[ ${ARTIFACT_NAME##*.} = 'zip' ]] ; then
140 | echo 'application/zip'
141 | elif [[ ${ARTIFACT_NAME##*.} = 'sig' ]] ; then
142 | echo 'application/pgp-signature'
143 | else
144 | echo 'text/plain'
145 | fi
146 |
147 | - name: upload-release-asset
148 | uses: actions/upload-release-asset@v1
149 | env:
150 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
151 | with:
152 | upload_url: ${{ needs.create-release.outputs.release_upload_url }}
153 | asset_path: ./${{ matrix.artifact }}
154 | asset_name: ${{ matrix.artifact }}
155 | asset_content_type: ${{ steps.determine-content-type.outputs.content_type }}
156 |
--------------------------------------------------------------------------------
/pkg/provider/team.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | "github.com/concourse/concourse/atc"
8 | "github.com/concourse/concourse/go-concourse/concourse"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11 | )
12 |
13 | var roleNames = []string{
14 | "owner",
15 | "member",
16 | "pipeline-operator",
17 | "viewer",
18 | }
19 |
20 | var roleTypes = []string{
21 | "users",
22 | "groups",
23 | }
24 |
25 | func dataTeam() *schema.Resource {
26 | return &schema.Resource{
27 | ReadContext: dataTeamRead,
28 |
29 | Schema: map[string]*schema.Schema{
30 | "team_name": &schema.Schema{
31 | Type: schema.TypeString,
32 | Required: true,
33 | },
34 |
35 | "owners": &schema.Schema{
36 | Type: schema.TypeSet,
37 | Computed: true,
38 | Set: schema.HashString,
39 | Elem: &schema.Schema{
40 | Type: schema.TypeString,
41 | },
42 | },
43 |
44 | "members": &schema.Schema{
45 | Type: schema.TypeSet,
46 | Computed: true,
47 | Set: schema.HashString,
48 | Elem: &schema.Schema{
49 | Type: schema.TypeString,
50 | },
51 | },
52 |
53 | "pipeline_operators": &schema.Schema{
54 | Type: schema.TypeSet,
55 | Computed: true,
56 | Set: schema.HashString,
57 | Elem: &schema.Schema{
58 | Type: schema.TypeString,
59 | },
60 | },
61 |
62 | "viewers": &schema.Schema{
63 | Type: schema.TypeSet,
64 | Computed: true,
65 | Set: schema.HashString,
66 | Elem: &schema.Schema{
67 | Type: schema.TypeString,
68 | },
69 | },
70 | },
71 | }
72 | }
73 |
74 | func resourceTeam() *schema.Resource {
75 | return &schema.Resource{
76 | CreateContext: resourceTeamCreate,
77 | ReadContext: resourceTeamRead,
78 | UpdateContext: resourceTeamUpdate,
79 | DeleteContext: resourceTeamDelete,
80 | Importer: &schema.ResourceImporter{
81 | StateContext: schema.ImportStatePassthroughContext,
82 | },
83 |
84 | Schema: map[string]*schema.Schema{
85 |
86 | "team_name": &schema.Schema{
87 | Type: schema.TypeString,
88 | Required: true,
89 | },
90 |
91 | "owners": &schema.Schema{
92 | Type: schema.TypeSet,
93 | Required: true,
94 | Set: schema.HashString,
95 | DefaultFunc: func() (interface{}, error) {
96 | return make([]string, 0), nil
97 | },
98 | Elem: &schema.Schema{
99 | Type: schema.TypeString,
100 | },
101 | },
102 |
103 | "members": &schema.Schema{
104 | Type: schema.TypeSet,
105 | Optional: true,
106 | Set: schema.HashString,
107 | DefaultFunc: func() (interface{}, error) {
108 | return make([]string, 0), nil
109 | },
110 | Elem: &schema.Schema{
111 | Type: schema.TypeString,
112 | },
113 | },
114 |
115 | "pipeline_operators": &schema.Schema{
116 | Type: schema.TypeSet,
117 | Optional: true,
118 | Set: schema.HashString,
119 | DefaultFunc: func() (interface{}, error) {
120 | return make([]string, 0), nil
121 | },
122 | Elem: &schema.Schema{
123 | Type: schema.TypeString,
124 | },
125 | },
126 |
127 | "viewers": &schema.Schema{
128 | Type: schema.TypeSet,
129 | Optional: true,
130 | Set: schema.HashString,
131 | DefaultFunc: func() (interface{}, error) {
132 | return make([]string, 0), nil
133 | },
134 | Elem: &schema.Schema{
135 | Type: schema.TypeString,
136 | },
137 | },
138 | },
139 |
140 | SchemaVersion: 1,
141 | StateUpgraders: []schema.StateUpgrader{
142 | {
143 | Type: resourceTeamResourceV0().CoreConfigSchema().ImpliedType(),
144 | Upgrade: resourceTeamStateUpgradeV0,
145 | Version: 0,
146 | },
147 | },
148 | }
149 | }
150 |
151 | type teamHelper struct {
152 | TeamName string
153 | Owners []interface{}
154 | Members []interface{}
155 | PipelineOperators []interface{}
156 | Viewers []interface{}
157 | }
158 |
159 | func (t *teamHelper) appendElem(field string, elem string) {
160 | switch field {
161 | case "owner":
162 | t.Owners = append(t.Owners, elem)
163 | case "member":
164 | t.Members = append(t.Members, elem)
165 | case "pipeline-operator":
166 | t.PipelineOperators = append(t.PipelineOperators, elem)
167 | case "viewer":
168 | t.Viewers = append(t.Viewers, elem)
169 | }
170 | }
171 |
172 | func readTeam(
173 | ctx context.Context,
174 | client concourse.Client,
175 | teamName string,
176 | ) (teamHelper, diag.Diagnostics) {
177 |
178 | team, err := client.FindTeam(teamName)
179 |
180 | retVal := teamHelper{
181 | TeamName: teamName,
182 | }
183 |
184 | if err != nil {
185 | return retVal, diag.FromErr(err)
186 | }
187 |
188 | if team == nil {
189 | return retVal, diag.Errorf("Could not find team %s", teamName)
190 | }
191 |
192 | var (
193 | ok bool
194 | role map[string][]string
195 | )
196 |
197 | for _, roleName := range roleNames {
198 | if role, ok = team.Auth()[roleName]; !ok {
199 | continue
200 | }
201 |
202 | users, user_ok := role["users"]
203 | groups, group_ok := role["groups"]
204 |
205 | if user_ok {
206 | for _, user := range users {
207 | retVal.appendElem(roleName, "user:"+user)
208 | }
209 | }
210 |
211 | if group_ok {
212 | for _, group := range groups {
213 | retVal.appendElem(roleName, "group:"+group)
214 | }
215 | }
216 | }
217 |
218 | return retVal, nil
219 | }
220 |
221 | func dataTeamRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
222 | client := m.(*ProviderConfig).Client
223 | teamName := d.Get("team_name").(string)
224 |
225 | team, err := readTeam(ctx, client, teamName)
226 |
227 | if err != nil {
228 | return err
229 | }
230 |
231 | d.SetId(team.TeamName)
232 | d.Set("team_name", team.TeamName)
233 | d.Set("owners", schema.NewSet(schema.HashString, team.Owners))
234 | d.Set("members", schema.NewSet(schema.HashString, team.Members))
235 | d.Set("pipeline_operators", schema.NewSet(schema.HashString, team.PipelineOperators))
236 | d.Set("viewers", schema.NewSet(schema.HashString, team.Viewers))
237 | return nil
238 | }
239 |
240 | func resourceTeamCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
241 | return resourceTeamCreateUpdate(ctx, d, m, true)
242 | }
243 |
244 | func resourceTeamUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
245 | return resourceTeamCreateUpdate(ctx, d, m, false)
246 | }
247 |
248 | func resourceTeamRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
249 | client := m.(*ProviderConfig).Client
250 | team, err := readTeam(ctx, client, d.Id())
251 |
252 | if err != nil {
253 | return err
254 | }
255 |
256 | d.SetId(team.TeamName)
257 | d.Set("team_name", team.TeamName)
258 | d.Set("owners", schema.NewSet(schema.HashString, team.Owners))
259 | d.Set("members", schema.NewSet(schema.HashString, team.Members))
260 | d.Set("pipeline_operators", schema.NewSet(schema.HashString, team.PipelineOperators))
261 | d.Set("viewers", schema.NewSet(schema.HashString, team.Viewers))
262 | return nil
263 | }
264 |
265 | func resourceTeamCreateUpdate(ctx context.Context, d *schema.ResourceData, m interface{}, create bool) diag.Diagnostics {
266 | client := m.(*ProviderConfig).Client
267 | teamName := d.Get("team_name").(string)
268 | auths := make(map[string][]string)
269 |
270 | var authline []string
271 | roleEnabled := make(map[string]bool)
272 |
273 | // fetches input from terraform and breaks out user/groups that prepend the string
274 | for _, role := range roleNames {
275 |
276 | // concourse calls things: "pipeline-operator", terraform calls them: "pipeline_operators"
277 | terraformRoleName := strings.ReplaceAll(role, "-", "_") + "s"
278 |
279 | for _, terraformInput := range d.Get(terraformRoleName).(*schema.Set).List() {
280 | roleEnabled[role] = true
281 | authline = strings.Split(terraformInput.(string), ":")
282 | switch authline[0] {
283 | case "user":
284 | auths[role+"_users"] = append(auths[role+"_users"], strings.Join(authline[1:], ":"))
285 | case "group":
286 | auths[role+"_groups"] = append(auths[role+"_groups"], strings.Join(authline[1:], ":"))
287 | }
288 | }
289 | }
290 |
291 | teamDetails := atc.Team{
292 | Name: teamName,
293 | Auth: atc.TeamAuth{},
294 | }
295 |
296 | // we cant set a role into the TeamAuth struct if it doesnt exist
297 | // otherwise sending the atc.Team to concourse creates "role": null entries
298 | for _, role := range roleNames {
299 | if roleEnabled[role] == true {
300 | teamDetails.Auth[role] = map[string][]string{}
301 | for _, roleType := range roleTypes {
302 | roleValues := auths[role+"_"+roleType]
303 | if len(roleValues) > 0 {
304 | teamDetails.Auth[role][roleType] = roleValues
305 | }
306 | }
307 | }
308 | }
309 |
310 | team := client.Team(teamName)
311 |
312 | if d.HasChange("team_name") && !create {
313 | _, warnings, err := team.RenameTeam(d.Id(), d.Get("team_name").(string))
314 |
315 | if err != nil {
316 | return diag.Errorf("Could not rename team %s %s", teamName, SerializeWarnings(warnings))
317 | }
318 | }
319 |
320 | _, created, updated, warnings, err := team.CreateOrUpdate(teamDetails)
321 |
322 | if err != nil {
323 | return diag.Errorf("Error creating/updating team %s: %s %s", teamName, err, SerializeWarnings(warnings))
324 | }
325 |
326 | if !created && !updated {
327 | return diag.Errorf("Could not create or update team %s", teamName)
328 | }
329 |
330 | d.SetId(teamName)
331 | return resourceTeamRead(ctx, d, m)
332 | }
333 |
334 | func resourceTeamDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
335 | client := m.(*ProviderConfig).Client
336 | teamName := d.Get("team_name").(string)
337 |
338 | if teamName == "main" {
339 | return diag.Errorf("Cannot delete main team")
340 | }
341 |
342 | team := client.Team(teamName)
343 |
344 | err := team.DestroyTeam(teamName)
345 |
346 | if err != nil {
347 | return diag.Errorf("Could not delete team %s: %s", teamName, err)
348 | }
349 |
350 | d.SetId("")
351 | return nil
352 | }
353 |
--------------------------------------------------------------------------------
/pkg/provider/pipeline.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/concourse/concourse/go-concourse/concourse"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
13 | )
14 |
15 | func dataPipeline() *schema.Resource {
16 | return &schema.Resource{
17 | ReadContext: dataPipelineRead,
18 |
19 | Schema: map[string]*schema.Schema{
20 | "pipeline_name": &schema.Schema{
21 | Type: schema.TypeString,
22 | Required: true,
23 | },
24 |
25 | "team_name": &schema.Schema{
26 | Type: schema.TypeString,
27 | Required: true,
28 | },
29 |
30 | "is_exposed": &schema.Schema{
31 | Type: schema.TypeBool,
32 | Required: false,
33 | Computed: true,
34 | },
35 |
36 | "is_paused": &schema.Schema{
37 | Type: schema.TypeBool,
38 | Required: false,
39 | Computed: true,
40 | },
41 |
42 | "yaml": &schema.Schema{
43 | Type: schema.TypeString,
44 | Required: false,
45 | Computed: true,
46 | },
47 |
48 | "json": &schema.Schema{
49 | Type: schema.TypeString,
50 | Required: false,
51 | Computed: true,
52 | },
53 | },
54 | }
55 | }
56 |
57 | func resourcePipeline() *schema.Resource {
58 | return &schema.Resource{
59 | CreateContext: resourcePipelineCreate,
60 | ReadContext: resourcePipelineRead,
61 | UpdateContext: resourcePipelineUpdate,
62 | DeleteContext: resourcePipelineDelete,
63 |
64 | Importer: &schema.ResourceImporter{
65 | StateContext: schema.ImportStatePassthroughContext,
66 | },
67 |
68 | Schema: map[string]*schema.Schema{
69 | "pipeline_name": &schema.Schema{
70 | Type: schema.TypeString,
71 | Required: true,
72 | },
73 |
74 | "team_name": &schema.Schema{
75 | Type: schema.TypeString,
76 | Required: true,
77 | ForceNew: true,
78 | },
79 |
80 | "is_exposed": &schema.Schema{
81 | Type: schema.TypeBool,
82 | Required: true,
83 | },
84 |
85 | "is_paused": &schema.Schema{
86 | Type: schema.TypeBool,
87 | Required: true,
88 | },
89 |
90 | "pipeline_config_format": &schema.Schema{
91 | Type: schema.TypeString,
92 | Required: true,
93 | ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"json", "yaml"}, false)),
94 | },
95 |
96 | "pipeline_config": &schema.Schema{
97 | Type: schema.TypeString,
98 | Required: true,
99 | },
100 |
101 | "vars": &schema.Schema{
102 | Type: schema.TypeMap,
103 | Optional: true,
104 | },
105 |
106 | "json": &schema.Schema{
107 | Type: schema.TypeString,
108 | Computed: true,
109 | },
110 |
111 | "yaml": &schema.Schema{
112 | Type: schema.TypeString,
113 | Computed: true,
114 | },
115 | },
116 | }
117 | }
118 |
119 | type pipelineHelper struct {
120 | TeamName string
121 | PipelineName string
122 | IsExposed bool
123 | IsPaused bool
124 | JSON string
125 | YAML string
126 | ConfigVersion string
127 | }
128 |
129 | func pipelineID(teamName string, pipelineName string) string {
130 | return fmt.Sprintf("%s:%s", teamName, pipelineName)
131 | }
132 |
133 | func parsePipelineID(id string) (string, string, error) {
134 | parts := strings.SplitN(id, ":", 2)
135 | if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
136 | return "", "", fmt.Errorf("Unexpected ID format (%q). Expected team_name:pipeline_name", id)
137 | }
138 | return parts[0], parts[1], nil
139 | }
140 |
141 | func readPipeline(
142 | ctx context.Context,
143 | client concourse.Client,
144 | teamName string,
145 | pipelineName string,
146 | ) (pipelineHelper, bool, error) {
147 |
148 | retVal := pipelineHelper{
149 | TeamName: teamName,
150 | PipelineName: pipelineName,
151 | ConfigVersion: "0",
152 | }
153 |
154 | team := client.Team(teamName)
155 |
156 | pipeline, pipelineFound, err := team.Pipeline(pipelineName)
157 |
158 | if err != nil {
159 | return retVal, false, err
160 | }
161 |
162 | if !pipelineFound {
163 | return retVal, false, nil
164 | }
165 |
166 | atcConfig, version, pipelineCfgFound, err := team.PipelineConfig(
167 | pipelineName,
168 | )
169 |
170 | if err != nil {
171 | return retVal, false, fmt.Errorf(
172 | "Error looking up pipeline %s within team '%s': %s",
173 | pipelineName, teamName, err,
174 | )
175 | }
176 |
177 | if !pipelineCfgFound {
178 | return retVal, false, nil
179 | }
180 |
181 | pipelineCfg, err := json.Marshal(atcConfig)
182 | if err != nil {
183 | return retVal, false, nil
184 | }
185 |
186 | pipelineCfgJSON, err := JSONToJSON(string(pipelineCfg))
187 | if err != nil {
188 | return retVal, false, fmt.Errorf(
189 | "Encountered error parsing pipeline %s config within team '%s': %s",
190 | pipelineName, teamName, err,
191 | )
192 | }
193 |
194 | pipelineCfgYAML, err := JSONToYAML(pipelineCfgJSON)
195 |
196 | if err != nil {
197 | return retVal, false, fmt.Errorf(
198 | "Encountered error parsing pipeline %s config within team '%s': %s",
199 | pipelineName, teamName, err,
200 | )
201 | }
202 |
203 | retVal.IsExposed = pipeline.Public
204 | retVal.IsPaused = pipeline.Paused
205 | retVal.ConfigVersion = version
206 | retVal.JSON = pipelineCfgJSON
207 | retVal.YAML = pipelineCfgYAML
208 |
209 | return retVal, true, nil
210 | }
211 |
212 | func dataPipelineRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
213 | client := m.(*ProviderConfig).Client
214 | pipelineName := d.Get("pipeline_name").(string)
215 | teamName := d.Get("team_name").(string)
216 |
217 | pipeline, wasFound, err := readPipeline(ctx, client, teamName, pipelineName)
218 |
219 | if err != nil {
220 | return diag.Errorf(
221 | "Error reading pipeline %s from team '%s': %s",
222 | pipelineName, teamName, err,
223 | )
224 | }
225 |
226 | if wasFound {
227 | d.SetId(pipelineID(teamName, pipelineName))
228 | d.Set("is_exposed", pipeline.IsExposed)
229 | d.Set("is_paused", pipeline.IsPaused)
230 | d.Set("json", pipeline.JSON)
231 | d.Set("yaml", pipeline.YAML)
232 | } else {
233 | d.SetId("")
234 | }
235 |
236 | return nil
237 | }
238 |
239 | func resourcePipelineCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
240 | return resourcePipelineUpdate(ctx, d, m)
241 | }
242 |
243 | func resourcePipelineRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
244 | client := m.(*ProviderConfig).Client
245 | teamName, pipelineName, err := parsePipelineID(d.Id())
246 | if err != nil {
247 | return diag.FromErr(err)
248 | }
249 |
250 | pipeline, wasFound, err := readPipeline(ctx, client, teamName, pipelineName)
251 |
252 | if err != nil {
253 | return diag.Errorf(
254 | "Error reading pipeline %s from team '%s': %s",
255 | pipelineName, teamName, err,
256 | )
257 | }
258 |
259 | if wasFound {
260 | d.SetId(pipelineID(pipeline.TeamName, pipeline.PipelineName))
261 | d.Set("team_name", pipeline.TeamName)
262 | d.Set("pipeline_name", pipeline.PipelineName)
263 | d.Set("is_exposed", pipeline.IsExposed)
264 | d.Set("is_paused", pipeline.IsPaused)
265 | d.Set("json", pipeline.JSON)
266 | d.Set("yaml", pipeline.YAML)
267 | } else {
268 | d.SetId("")
269 | }
270 |
271 | return nil
272 | }
273 |
274 | func resourcePipelineUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
275 | client := m.(*ProviderConfig).Client
276 |
277 | if d.HasChange("pipeline_name") && d.Id() != "" {
278 | teamName := strings.SplitN(d.Id(), ":", 2)[0]
279 | oldPipelineName := strings.SplitN(d.Id(), ":", 2)[1]
280 | newPipelineName := d.Get("pipeline_name").(string)
281 |
282 | team := client.Team(teamName)
283 |
284 | _, warnings, err := team.RenamePipeline(oldPipelineName, newPipelineName)
285 |
286 | if err != nil {
287 | return diag.Errorf(
288 | "Error renaming pipeline %s to %s in team %s: %s %s",
289 | oldPipelineName, newPipelineName, teamName, err, SerializeWarnings(warnings),
290 | )
291 | }
292 | }
293 |
294 | pipelineName := d.Get("pipeline_name").(string)
295 | teamName := d.Get("team_name").(string)
296 | d.SetId(pipelineID(teamName, pipelineName))
297 | team := client.Team(teamName)
298 |
299 | pipelineConfig := d.Get("pipeline_config").(string)
300 | pipelineConfigFormat := d.Get("pipeline_config_format").(string)
301 | vars := d.Get("vars").(map[string]interface{})
302 |
303 | pipeline, _, err := readPipeline(ctx, client, teamName, pipelineName)
304 |
305 | if err != nil {
306 | return diag.Errorf(
307 | "Error looking up pipeline %s in team %s: %s",
308 | pipelineName, teamName, err,
309 | )
310 | }
311 |
312 | parsedJSON, err := ParsePipelineConfig(pipelineConfig, pipelineConfigFormat, vars)
313 |
314 | if err != nil {
315 | return diag.Errorf("Error parsing pipeline_config: %s", err)
316 | }
317 |
318 | _, _, configWarnings, err := team.CreateOrUpdatePipelineConfig(
319 | pipelineName, pipeline.ConfigVersion, []byte(parsedJSON), false,
320 | )
321 |
322 | if err != nil {
323 | return diag.Errorf(
324 | "Encountered error setting config for pipeline %s in team '%s': %s",
325 | pipelineName, teamName, err,
326 | )
327 | }
328 |
329 | if len(configWarnings) != 0 {
330 | warnings := ""
331 | for _, w := range configWarnings {
332 | warnings += fmt.Sprintf("%s: %s\n", w.Type, w.Message)
333 | }
334 |
335 | return diag.Errorf(
336 | "Encountered pipeline warnings (%s/%s):\n %s",
337 | pipelineName, teamName, warnings,
338 | )
339 | }
340 |
341 | if d.Get("is_exposed").(bool) {
342 | found, err := team.ExposePipeline(pipelineName)
343 | if err != nil {
344 | return diag.Errorf(
345 | "Error exposing pipeline %s in team '%s': %s",
346 | pipelineName, teamName, err,
347 | )
348 | }
349 | if !found {
350 | return diag.Errorf(
351 | "Could not find pipeline %s in team '%s': %s",
352 | pipelineName, teamName, err,
353 | )
354 | }
355 | } else {
356 | found, err := team.HidePipeline(pipelineName)
357 | if err != nil {
358 | return diag.Errorf(
359 | "Error hiding pipeline %s in team '%s': %s",
360 | pipelineName, teamName, err,
361 | )
362 | }
363 | if !found {
364 | return diag.Errorf(
365 | "Could not find pipeline %s in team '%s': %s",
366 | pipelineName, teamName, err,
367 | )
368 | }
369 | }
370 |
371 | if d.Get("is_paused").(bool) {
372 | found, err := team.PausePipeline(pipelineName)
373 | if err != nil {
374 | return diag.Errorf(
375 | "Error pausing pipeline %s in team '%s': %s",
376 | pipelineName, teamName, err,
377 | )
378 | }
379 | if !found {
380 | return diag.Errorf(
381 | "Could not find pipeline %s in team '%s': %s",
382 | pipelineName, teamName, err,
383 | )
384 | }
385 | } else {
386 | found, err := team.UnpausePipeline(pipelineName)
387 | if err != nil {
388 | return diag.Errorf(
389 | "Error unpausing pipeline %s in team '%s': %s",
390 | pipelineName, teamName, err,
391 | )
392 | }
393 | if !found {
394 | return diag.Errorf(
395 | "Could not find pipeline %s in team '%s': %s",
396 | pipelineName, teamName, err,
397 | )
398 | }
399 | }
400 |
401 | return resourcePipelineRead(ctx, d, m)
402 | }
403 |
404 | func resourcePipelineDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
405 | client := m.(*ProviderConfig).Client
406 | pipelineName := d.Get("pipeline_name").(string)
407 | teamName := d.Get("team_name").(string)
408 | team := client.Team(teamName)
409 |
410 | deleted, err := team.DeletePipeline(pipelineName)
411 |
412 | if err != nil {
413 | return diag.Errorf(
414 | "Could not delete pipeline %s from team %s: %s",
415 | pipelineName, teamName, err,
416 | )
417 | }
418 |
419 | if !deleted {
420 | return diag.Errorf(
421 | "Could not delete pipeline %s from team %s", pipelineName, teamName,
422 | )
423 | }
424 |
425 | d.SetId("")
426 | return nil
427 | }
428 |
--------------------------------------------------------------------------------
/integration/team_mgmt.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 |
7 | . "github.com/onsi/ginkgo"
8 | . "github.com/onsi/gomega"
9 |
10 | "github.com/alphagov/terraform-provider-concourse/pkg/provider"
11 |
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14 | "github.com/concourse/concourse/atc"
15 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
16 | )
17 |
18 | var _ = Describe("Team management", func() {
19 | BeforeEach(SetupTest)
20 | AfterEach(TeardownTest)
21 |
22 | It("should read all teams", func() {
23 | providers := map[string]*schema.Provider{
24 | "concourse": provider.Provider(),
25 | }
26 |
27 | resource.Test(NewGinkoTerraformTestingT(), resource.TestCase{
28 | IsUnitTest: false,
29 |
30 | Providers: providers,
31 |
32 | Steps: []resource.TestStep{
33 | resource.TestStep{
34 | Config: `data "concourse_teams" "teams" {}`,
35 | Check: resource.ComposeTestCheckFunc(
36 | resource.TestCheckResourceAttr("data.concourse_teams.teams", "names.#", "1"),
37 | resource.TestCheckResourceAttr("data.concourse_teams.teams", "names.0", "main"),
38 | ),
39 | },
40 | },
41 | })
42 | })
43 |
44 | It("should manage the lifecycle of a team", func() {
45 | providers := map[string]*schema.Provider{
46 | "concourse": provider.Provider(),
47 | }
48 |
49 | client, err := NewConcourseClient()
50 |
51 | Expect(err).NotTo(HaveOccurred())
52 |
53 | resource.Test(NewGinkoTerraformTestingT(), resource.TestCase{
54 | IsUnitTest: false,
55 |
56 | Providers: providers,
57 |
58 | Steps: []resource.TestStep{
59 | resource.TestStep{
60 | // Add a user as an owner
61 |
62 | Config: `resource "concourse_team" "a_team" {
63 | team_name = "team-a"
64 | owners = ["user:github:tlwr"]
65 | }`,
66 |
67 | Check: resource.ComposeTestCheckFunc(
68 | func(s *terraform.State) error {
69 | By("Adding a user as an owner")
70 |
71 | fmt.Printf("%+v\n", s)
72 | return nil
73 | },
74 |
75 | resource.TestCheckResourceAttr("concourse_team.a_team", "team_name", "team-a"),
76 |
77 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.#", "1"),
78 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.0", "user:github:tlwr"),
79 | resource.TestCheckResourceAttr("concourse_team.a_team", "members.#", "0"),
80 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.#", "0"),
81 | resource.TestCheckResourceAttr("concourse_team.a_team", "viewers.#", "0"),
82 |
83 | func(s *terraform.State) error {
84 | teams, err := client.ListTeams()
85 |
86 | if err != nil {
87 | return nil
88 | }
89 |
90 | Expect(teams).To(HaveLen(2))
91 |
92 | Expect(teams[0].Name).To(Equal("main"))
93 | Expect(teams[1].Name).To(Equal("team-a"))
94 |
95 | expectedTeamAuth := atc.TeamAuth{
96 | "owner": {"users": {"github:tlwr"}},
97 | }
98 |
99 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
100 |
101 | return nil
102 | },
103 | ),
104 | },
105 |
106 | resource.TestStep{
107 | // check this state is importable
108 | ImportState: true,
109 | ResourceName: "concourse_team.a_team",
110 | ImportStateVerify: true,
111 | },
112 |
113 | resource.TestStep{
114 | // Add another user as another owner
115 |
116 | Config: `resource "concourse_team" "a_team" {
117 | team_name = "team-a"
118 | owners = [
119 | "user:github:tlwr",
120 | "user:github:terraform-provider-concourse",
121 | ]
122 | }`,
123 |
124 | Check: resource.ComposeTestCheckFunc(
125 | func(s *terraform.State) error {
126 | By("Adding a user as an owner")
127 |
128 | fmt.Printf("%+v\n", s)
129 | return nil
130 | },
131 |
132 | resource.TestCheckResourceAttr("concourse_team.a_team", "team_name", "team-a"),
133 |
134 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.#", "2"),
135 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.0", "user:github:terraform-provider-concourse"),
136 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.1", "user:github:tlwr"),
137 | resource.TestCheckResourceAttr("concourse_team.a_team", "members.#", "0"),
138 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.#", "0"),
139 | resource.TestCheckResourceAttr("concourse_team.a_team", "viewers.#", "0"),
140 |
141 | func(s *terraform.State) error {
142 | teams, err := client.ListTeams()
143 |
144 | if err != nil {
145 | return nil
146 | }
147 |
148 | Expect(teams).To(HaveLen(2))
149 |
150 | Expect(teams[0].Name).To(Equal("main"))
151 | Expect(teams[1].Name).To(Equal("team-a"))
152 |
153 | expectedTeamAuth := atc.TeamAuth{
154 | "owner": {"users": {
155 | "github:terraform-provider-concourse",
156 | "github:tlwr",
157 | }},
158 | }
159 |
160 | sort.Strings(teams[1].Auth["owner"]["users"])
161 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
162 |
163 | return nil
164 | },
165 | ),
166 | },
167 |
168 | resource.TestStep{
169 | // check this state is importable
170 | ImportState: true,
171 | ResourceName: "concourse_team.a_team",
172 | ImportStateVerify: true,
173 | },
174 |
175 | resource.TestStep{
176 | // Change a user from an owner to a pipeline-operator
177 |
178 | Config: `resource "concourse_team" "a_team" {
179 | team_name = "team-a"
180 |
181 | owners = ["user:github:terraform-provider-concourse"]
182 | pipeline_operators = ["user:github:tlwr"]
183 | }`,
184 |
185 | Check: resource.ComposeTestCheckFunc(
186 | func(s *terraform.State) error {
187 | By("Changing a user from an owner to a pipeline-operator")
188 |
189 | fmt.Printf("%+v\n", s)
190 | return nil
191 | },
192 |
193 | resource.TestCheckResourceAttr("concourse_team.a_team", "team_name", "team-a"),
194 |
195 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.#", "1"),
196 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.0", "user:github:terraform-provider-concourse"),
197 | resource.TestCheckResourceAttr("concourse_team.a_team", "members.#", "0"),
198 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.#", "1"),
199 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.0", "user:github:tlwr"),
200 | resource.TestCheckResourceAttr("concourse_team.a_team", "viewers.#", "0"),
201 |
202 | func(s *terraform.State) error {
203 | teams, err := client.ListTeams()
204 |
205 | if err != nil {
206 | return nil
207 | }
208 |
209 | Expect(teams).To(HaveLen(2))
210 |
211 | Expect(teams[0].Name).To(Equal("main"))
212 | Expect(teams[1].Name).To(Equal("team-a"))
213 |
214 | expectedTeamAuth := atc.TeamAuth{
215 | "pipeline-operator": {
216 | "users": {
217 | "github:tlwr",
218 | },
219 | },
220 | "owner": {
221 | "users": {
222 | "github:terraform-provider-concourse",
223 | },
224 | },
225 | }
226 |
227 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
228 |
229 | return nil
230 | },
231 | ),
232 | },
233 |
234 | resource.TestStep{
235 | // check this state is importable
236 | ImportState: true,
237 | ResourceName: "concourse_team.a_team",
238 | ImportStateVerify: true,
239 | },
240 |
241 | resource.TestStep{
242 | // Removing a user, adding a group
243 |
244 | Config: `resource "concourse_team" "a_team" {
245 | team_name = "team-a"
246 |
247 | owners = [
248 | "user:github:terraform-provider-concourse",
249 | "group:github:alphagov:paas-team",
250 | ]
251 | }`,
252 |
253 | Check: resource.ComposeTestCheckFunc(
254 | func(s *terraform.State) error {
255 | By("Removing a user, adding a group")
256 |
257 | fmt.Printf("%+v\n", s)
258 | return nil
259 | },
260 |
261 | resource.TestCheckResourceAttr("concourse_team.a_team", "team_name", "team-a"),
262 |
263 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.#", "2"),
264 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.0", "group:github:alphagov:paas-team"),
265 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.1", "user:github:terraform-provider-concourse"),
266 | resource.TestCheckResourceAttr("concourse_team.a_team", "members.#", "0"),
267 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.#", "0"),
268 | resource.TestCheckResourceAttr("concourse_team.a_team", "viewers.#", "0"),
269 |
270 | func(s *terraform.State) error {
271 | teams, err := client.ListTeams()
272 |
273 | if err != nil {
274 | return nil
275 | }
276 |
277 | Expect(teams).To(HaveLen(2))
278 |
279 | Expect(teams[0].Name).To(Equal("main"))
280 | Expect(teams[1].Name).To(Equal("team-a"))
281 |
282 | expectedTeamAuth := atc.TeamAuth{
283 | "owner": {
284 | "users": {"github:terraform-provider-concourse"},
285 | "groups": {"github:alphagov:paas-team"},
286 | },
287 | }
288 |
289 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
290 |
291 | return nil
292 | },
293 | ),
294 | },
295 |
296 | resource.TestStep{
297 | // check this state is importable
298 | ImportState: true,
299 | ResourceName: "concourse_team.a_team",
300 | ImportStateVerify: true,
301 | },
302 |
303 | resource.TestStep{
304 | // New team
305 |
306 | Config: `resource "concourse_team" "new_team" {
307 | team_name = "team-new"
308 |
309 | pipeline_operators = ["user:github:tlwr"]
310 | }`,
311 |
312 | Check: resource.ComposeTestCheckFunc(
313 | func(s *terraform.State) error {
314 | By("New team")
315 |
316 | fmt.Printf("%+v\n", s)
317 | return nil
318 | },
319 |
320 | resource.TestCheckResourceAttr("concourse_team.new_team", "team_name", "team-new"),
321 |
322 | resource.TestCheckResourceAttr("concourse_team.new_team", "owners.#", "0"),
323 | resource.TestCheckResourceAttr("concourse_team.new_team", "members.#", "0"),
324 | resource.TestCheckResourceAttr("concourse_team.new_team", "pipeline_operators.#", "1"),
325 | resource.TestCheckResourceAttr("concourse_team.new_team", "pipeline_operators.0", "user:github:tlwr"),
326 | resource.TestCheckResourceAttr("concourse_team.new_team", "viewers.#", "0"),
327 |
328 | func(s *terraform.State) error {
329 | teams, err := client.ListTeams()
330 |
331 | if err != nil {
332 | return nil
333 | }
334 |
335 | Expect(teams).To(HaveLen(2))
336 |
337 | Expect(teams[0].Name).To(Equal("main"))
338 | Expect(teams[1].Name).To(Equal("team-new"))
339 |
340 | expectedTeamAuth := atc.TeamAuth{
341 | "pipeline-operator": {"users": {"github:tlwr"}},
342 | }
343 |
344 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
345 |
346 | return nil
347 | },
348 | ),
349 | },
350 |
351 | resource.TestStep{
352 | // check this state is importable
353 | ImportState: true,
354 | ResourceName: "concourse_team.new_team",
355 | ImportStateVerify: true,
356 | },
357 |
358 | resource.TestStep{
359 | // Rename the team
360 |
361 | Config: `resource "concourse_team" "a_team" {
362 | team_name = "team-a-renamed"
363 |
364 | pipeline_operators = ["user:github:tlwr"]
365 | }`,
366 |
367 | Check: resource.ComposeTestCheckFunc(
368 | func(s *terraform.State) error {
369 | By("Renaming the team")
370 |
371 | fmt.Printf("%+v\n", s)
372 | return nil
373 | },
374 |
375 | resource.TestCheckResourceAttr("concourse_team.a_team", "team_name", "team-a-renamed"),
376 |
377 | resource.TestCheckResourceAttr("concourse_team.a_team", "owners.#", "0"),
378 | resource.TestCheckResourceAttr("concourse_team.a_team", "members.#", "0"),
379 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.#", "1"),
380 | resource.TestCheckResourceAttr("concourse_team.a_team", "pipeline_operators.0", "user:github:tlwr"),
381 | resource.TestCheckResourceAttr("concourse_team.a_team", "viewers.#", "0"),
382 |
383 | func(s *terraform.State) error {
384 | teams, err := client.ListTeams()
385 |
386 | if err != nil {
387 | return nil
388 | }
389 |
390 | Expect(teams).To(HaveLen(2))
391 |
392 | Expect(teams[0].Name).To(Equal("main"))
393 | Expect(teams[1].Name).To(Equal("team-a-renamed"))
394 |
395 | expectedTeamAuth := atc.TeamAuth{
396 | "pipeline-operator": {"users": {"github:tlwr"}},
397 | }
398 |
399 | Expect(teams[1].Auth).To(Equal(expectedTeamAuth))
400 |
401 | return nil
402 | },
403 | ),
404 | },
405 |
406 | resource.TestStep{
407 | // check this state is importable
408 | ImportState: true,
409 | ResourceName: "concourse_team.a_team",
410 | ImportStateVerify: true,
411 | },
412 |
413 | resource.TestStep{
414 | // Delete the team
415 |
416 | Config: `# Cannot be empty`,
417 |
418 | Check: resource.ComposeTestCheckFunc(
419 | func(s *terraform.State) error {
420 | By("Deleting the team")
421 |
422 | fmt.Printf("%+v\n", s)
423 | return nil
424 | },
425 |
426 | func(s *terraform.State) error {
427 | Expect(s.RootModule().Resources).To(HaveLen(0))
428 | return nil
429 | },
430 |
431 | func(s *terraform.State) error {
432 | teams, err := client.ListTeams()
433 |
434 | if err != nil {
435 | return nil
436 | }
437 |
438 | Expect(teams).To(HaveLen(1))
439 |
440 | Expect(teams[0].Name).To(Equal("main"))
441 |
442 | return nil
443 | },
444 | ),
445 | },
446 | },
447 |
448 | CheckDestroy: resource.ComposeTestCheckFunc(
449 | func(s *terraform.State) error {
450 | teams, err := client.ListTeams()
451 |
452 | if err != nil {
453 | return nil
454 | }
455 |
456 | Expect(teams).To(HaveLen(1))
457 |
458 | Expect(teams[0].Name).To(Equal("main"))
459 |
460 | return nil
461 | },
462 | ),
463 | })
464 | })
465 | })
466 |
--------------------------------------------------------------------------------
/integration/pipeline_mgmt.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "fmt"
5 |
6 | . "github.com/onsi/ginkgo"
7 | . "github.com/onsi/gomega"
8 |
9 | "github.com/alphagov/terraform-provider-concourse/pkg/provider"
10 |
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
14 | )
15 |
16 | var _ = Describe("Pipeline management", func() {
17 | BeforeEach(SetupTest)
18 | AfterEach(TeardownTest)
19 |
20 | const (
21 | pipelineConfig = `#
22 | resources:
23 | - name: every-midnight
24 | type: time
25 | source:
26 | location: Europe/London
27 | start: 12:00AM
28 | stop: 12:15AM
29 |
30 | jobs:
31 | - name: check-the-time
32 | serial: true
33 | plan:
34 | - get: every-midnight
35 | trigger: true
36 | `
37 |
38 | pipelineConfigJSON = `{"jobs":[{"name":"check-the-time","plan":[{"get":"every-midnight","trigger":true}],"serial":true}],"resources":[{"name":"every-midnight","source":{"location":"Europe/London","start":"12:00AM","stop":"12:15AM"},"type":"time"}]}`
39 |
40 | templatedPipelineConfig = `#
41 | resources:
42 | - name: every-midnight
43 | type: time
44 | source:
45 | location: ((location))
46 | start: 12:00AM
47 | stop: 12:15AM
48 |
49 | jobs:
50 | - name: check-the-time
51 | serial: true
52 | plan:
53 | - get: every-midnight
54 | trigger: true
55 | `
56 |
57 | templatedPipelineConfigJSON = `{"jobs":[{"name":"check-the-time","plan":[{"get":"every-midnight","trigger":true}],"serial":true}],"resources":[{"name":"every-midnight","source":{"location":"Europe/Berlin","start":"12:00AM","stop":"12:15AM"},"type":"time"}]}`
58 |
59 | updatedPipelineConfig = `#
60 | resources:
61 | - name: every-midnight
62 | type: time
63 | source:
64 | location: Europe/London
65 | start: 12:00AM
66 | stop: 12:15AM
67 |
68 | jobs:
69 | - name: check-the-time-on-demand
70 | serial: true
71 | plan:
72 | - get: every-midnight
73 | `
74 | updatedPipelineConfigJSON = `{"jobs":[{"name":"check-the-time-on-demand","plan":[{"get":"every-midnight"}],"serial":true}],"resources":[{"name":"every-midnight","source":{"location":"Europe/London","start":"12:00AM","stop":"12:15AM"},"type":"time"}]}`
75 | )
76 |
77 | It("should manage the lifecycle of a pipeline", func() {
78 | providers := map[string]*schema.Provider{
79 | "concourse": provider.Provider(),
80 | }
81 |
82 | client, err := NewConcourseClient()
83 |
84 | Expect(err).NotTo(HaveOccurred())
85 |
86 | resource.Test(NewGinkoTerraformTestingT(), resource.TestCase{
87 | IsUnitTest: false,
88 |
89 | Providers: providers,
90 |
91 | Steps: []resource.TestStep{
92 | resource.TestStep{
93 | // Add a pipeline
94 |
95 | Config: fmt.Sprintf(`data "concourse_team" "main_team" {
96 | team_name = "main"
97 | }
98 |
99 | resource "concourse_team" "other_team" {
100 | team_name = "other"
101 | owners = ["user:github:tlwr"]
102 | }
103 |
104 | resource "concourse_pipeline" "a_pipeline" {
105 | team_name = "${data.concourse_team.main_team.team_name}"
106 | pipeline_name = "pipeline-a"
107 |
108 | is_exposed = false
109 | is_paused = false
110 |
111 | pipeline_config_format = "yaml"
112 | pipeline_config = <