├── 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 | 22 | 24 | 53 | 58 | 63 | 64 | 66 | 67 | 69 | image/svg+xml 70 | 72 | 73 | 74 | 75 | 76 | 81 | 86 | 91 | 97 | 98 | 99 | 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 = <