From 9bae2e95df87dbd4acae11deb0765be7256fd141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=BCrzeder?= Date: Mon, 6 Sep 2021 13:49:11 +0200 Subject: [PATCH] Authentication must be handled by the app, due to GitHub limitations (#9) --- .github/workflows/docker-build-pr.yaml | 16 ------- .github/workflows/docker-build-release.yaml | 53 --------------------- .github/workflows/unit-tests.yaml | 53 ++++++++------------- Dockerfile | 5 +- README.md | 36 ++++++++++---- action.yml | 5 +- examples/Advanced.md | 13 ++--- examples/advanced-example.yaml | 11 ++--- main.go | 2 - pkg/azure/azure.go | 51 +++++++------------- pkg/github/actions/authenticate.go | 6 +-- pkg/github/actions/deploy.go | 18 ++----- pkg/github/options.go | 12 +++-- version.go | 28 ----------- 14 files changed, 89 insertions(+), 220 deletions(-) delete mode 100644 .github/workflows/docker-build-pr.yaml delete mode 100644 .github/workflows/docker-build-release.yaml delete mode 100644 version.go diff --git a/.github/workflows/docker-build-pr.yaml b/.github/workflows/docker-build-pr.yaml deleted file mode 100644 index 3e9eb64..0000000 --- a/.github/workflows/docker-build-pr.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Build Image - -on: - pull_request: - push: - branches: - - '*' - - '*/*' - - '!master' - -jobs: - update: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: docker/build-push-action@v2.6.1 \ No newline at end of file diff --git a/.github/workflows/docker-build-release.yaml b/.github/workflows/docker-build-release.yaml deleted file mode 100644 index cfec97e..0000000 --- a/.github/workflows/docker-build-release.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Build and push Image - Release - -on: - push: - branches: - - "master" - tags: - - '*' - schedule: - - cron: '0 2 * * 0' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@master - - - uses: docker/metadata-action@v3 - id: meta - name: Docker meta - with: - images: ghcr.io/whiteducksoftware/azure-arm-action - tags: | - type=ref,event=branch - type=ref,event=tag - - - uses: docker/setup-qemu-action@v1 - name: Set up QEMU - - - uses: docker/setup-buildx-action@v1 - name: Set up Docker Buildx - - - uses: docker/login-action@v1 - name: Login to DockerHub - with: - registry: ghcr.io - username: ${{ secrets.GCR_USERNAME }} - password: ${{ secrets.GCR_PASSWORD }} - - - uses: actions/github-script@v3 - id: build_args - with: - result-encoding: string - script: return `GIT_SHA=${process.env.GITHUB_SHA}` - - - uses: docker/build-push-action@v2.6.1 - with: - context: . - platforms: linux/amd64 - build-args: ${{ steps.build_args.outputs.result }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} \ No newline at end of file diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 152c6ca..a4edc7f 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -7,40 +7,29 @@ jobs: test_action_job: runs-on: ubuntu-latest steps: - - name: Login to Azure - uses: Azure/login@v1 + - name: Check out Source Code + uses: actions/checkout@v1 + + - uses: ./ + id: deploy with: creds: ${{ secrets.AZURE_CREDENTIALS }} + resourceGroupName: azurearmaction + templateLocation: examples/template/template.json + parameters: examples/template/parameters.json + deploymentName: github-advanced-test - - name: Set up Go 1.16 - uses: actions/setup-go@v1 - with: - go-version: 1.16 + - run: echo ${{ steps.deploy.outputs.containerName }} - - name: Check out source code - uses: actions/checkout@v1 - - - name: Build - env: - GOPROXY: "https://proxy.golang.org" - CGO_ENABLED: 0 - GOOS: linux - GOARCH: amd64 - run: go build -a -installsuffix cgo -ldflags="-w -s" . + - uses: ./ + id: deploy2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + resourceGroupName: azurearmaction + templateLocation: examples/template/template.json + parameters: examples/template/parameters.json + deploymentName: github-advanced-test + overrideParameters: | + containerName=${{ steps.deploy.outputs.containerName }}-overriden - - name: Test - env: - GOPROXY: "https://proxy.golang.org" - CGO_ENABLED: 0 - GOOS: linux - GOARCH: amd64 - LOG_LEVEL: DEBUG - INPUT_RESOURCEGROUPNAME: azurearmaction - INPUT_TEMPLATELOCATION: ./test/template.json - INPUT_PARAMETERS: ./test/parameters.json - INPUT_OVERRIDEPARAMETERS: | - containerName=github-action-overriden - connectionString='Server=tcp:test.database.windows.net;Database=test;User ID=test;Password=test;Trusted_Connection=False;Encrypt=True;' - INPUT_DEPLOYMENTNAME: github-test - INPUT_DEPLOYMENTMODE: Incremental - run: go test -v -failfast . \ No newline at end of file + - run: echo ${{ steps.deploy2.outputs.containerName }} diff --git a/Dockerfile b/Dockerfile index 056781e..fc94a74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app # Git is required for fetching the dependencies. # Ca-certificates is required to call HTTPS endpoints. RUN apk update && \ - apk add --no-cache git ca-certificates upx && \ + apk add --no-cache git ca-certificates && \ update-ca-certificates # Add src files @@ -19,8 +19,7 @@ RUN go mod verify # Build the binary. ARG GIT_SHA RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -ldflags="-w -s -X main.gitSha=${GIT_SHA} -X main.goVersion=$(go version | cut -d " " -f 3) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -a -o /go/bin/azure-arm-action \ - && upx -q /go/bin/azure-arm-action + go build -a -o /go/bin/azure-arm-action # Runner FROM scratch diff --git a/README.md b/README.md index a60b06d..01f33d4 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ A GitHub Action to deploy ARM templates. ## Dependencies * [Checkout](https://github.com/actions/checkout) To checks-out your repository so the workflow can access any specified ARM template. -* [Azure/Login](https://github.com/Azure/login) To authenticate with Azure. ## Inputs +* `creds` **Required** + [Create Service Principal for Authentication](#Create-Service-Principal-for-Authentication) + * `templateLocation` **Required** Specify the path to the Azure Resource Manager template. (See [assets/json/template.json](test/template.json)) @@ -45,15 +47,34 @@ Additionally are the following outputs available: ## Usage ```yml -- uses: whiteducksoftware/azure-arm-action@v3.3 +- uses: whiteducksoftware/azure-arm-action@master with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: templateLocation: deploymentName: ``` -## Example +## Create Service Principal for Authentication +The Service Principal can be easily generated using the Azure CLI. Using the following command will create the SP in the supported structure. +At Subscription Scope: `az ad sp create-for-rbac --name "azure-arm-action" --role contributor --scopes=/subscriptions/********-****-****-****-************/ --sdk-auth -o json` +The JSON, which shall be used for authentication, should be in the following format: +```json +{ + "clientId": "********-****-****-****-************", + "clientSecret": "[*]", + "subscriptionId": "********-****-****-****-************", + "tenantId": "********-****-****-****-************", + "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", + "resourceManagerEndpointUrl": "https://management.azure.com/", + "activeDirectoryGraphResourceId": "https://graph.windows.net/", + "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", + "galleryEndpointUrl": "https://gallery.azure.com/", + "managementEndpointUrl": "https://management.core.windows.net/" +} +``` +## Example ```yml on: [push] name: ARMActionSample @@ -63,14 +84,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - - name: Login to Azure - uses: Azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - uses: whiteducksoftware/azure-arm-action@v3.3 + - uses: whiteducksoftware/azure-arm-action@master with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: templateLocation: parameters: OR diff --git a/action.yml b/action.yml index c863a64..cac78e7 100644 --- a/action.yml +++ b/action.yml @@ -14,9 +14,6 @@ inputs: deploymentName: description: "Specifies the name of the resource group deployment to create." required: true - subscriptionId: - description: "Specify the Subscription Id where you want to deploy your template. If not set the Id will be read from the CLI." - required: false deploymentMode: description: "Incremental (only add resources to resource group) or Complete (remove extra resources from resource group)." required: false @@ -35,4 +32,4 @@ branding: icon: package runs: using: 'docker' - image: 'docker://ghcr.io/whiteducksoftware/azure-arm-action:v3.3' \ No newline at end of file + image: 'Dockerfile' \ No newline at end of file diff --git a/examples/Advanced.md b/examples/Advanced.md index f9d83ab..4951dd7 100644 --- a/examples/Advanced.md +++ b/examples/Advanced.md @@ -4,16 +4,10 @@ Our template has two outputs `location` and `containerName`. But we are only int ## Steps ```yaml -- name: Login to Azure - uses: Azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} -``` -As first step we need to authenticate with Azure. -```yaml -- uses: whiteducksoftware/azure-arm-action@v3.3 +- uses: whiteducksoftware/azure-arm-action@master id: deploy with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: azurearmaction templateLocation: examples/template/template.json parameters: examples/template/parameters.json @@ -42,9 +36,10 @@ we can see that on the console will be `github-action` printed. Now we add our second deployment which relies on that value and modfies the `containerName` parameter, ```yaml -- uses: whiteducksoftware/azure-arm-action@v3.3 +- uses: whiteducksoftware/azure-arm-action@master id: deploy2 with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: azurearmaction templateLocation: examples/template/template.json parameters: examples/template/parameters.json diff --git a/examples/advanced-example.yaml b/examples/advanced-example.yaml index 688da77..1518c28 100644 --- a/examples/advanced-example.yaml +++ b/examples/advanced-example.yaml @@ -11,14 +11,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Login to Azure - uses: Azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - uses: whiteducksoftware/azure-arm-action@v3.3 + - uses: whiteducksoftware/azure-arm-action@master id: deploy with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: azurearmaction templateLocation: examples/template/template.json parameters: examples/template/parameters.json @@ -26,9 +22,10 @@ jobs: - run: echo ${{ steps.deploy.outputs.containerName }} - - uses: whiteducksoftware/azure-arm-action@v3.3 + - uses: whiteducksoftware/azure-arm-action@master id: deploy2 with: + creds: ${{ secrets.AZURE_CREDENTIALS }} resourceGroupName: azurearmaction templateLocation: examples/template/template.json parameters: examples/template/parameters.json diff --git a/main.go b/main.go index fa0bc71..1ef39b8 100644 --- a/main.go +++ b/main.go @@ -31,8 +31,6 @@ func init() { } func main() { - logrus.Info(__Version__) - opts, err := github.LoadOptions() if err != nil { logrus.Errorf("failed to load options: %s", err) diff --git a/pkg/azure/azure.go b/pkg/azure/azure.go index 387b9d9..a75d4c4 100644 --- a/pkg/azure/azure.go +++ b/pkg/azure/azure.go @@ -11,7 +11,6 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources" "github.com/Azure/go-autorest/autorest" @@ -28,6 +27,7 @@ type SDKAuth struct { SubscriptionID string `json:"subscriptionId"` TenantID string `json:"tenantId"` ARMEndpointURL string `json:"resourceManagerEndpointUrl"` + ADEndpointURL string `json:"activeDirectoryEndpointUrl"` } // GetSdkAuthFromString builds from the cmd flags a ServicePrincipal @@ -42,8 +42,13 @@ func GetSdkAuthFromString(credentials string) (SDKAuth, error) { } // GetArmAuthorizerFromSdkAuth creates an ARM authorizer from an Sp -func GetArmAuthorizerFromSdkAuth(auth SDKAuth) (*autorest.Authorizer, error) { - oauthconfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, auth.TenantID) +func GetArmAuthorizerFromSdkAuth(auth SDKAuth) (autorest.Authorizer, error) { + // If the Active Directory Endpoint is not set, fallback to the default public cloud endpoint + if len(auth.ADEndpointURL) == 0 { + auth.ADEndpointURL = azure.PublicCloud.ActiveDirectoryEndpoint + } + + oauthConfig, err := adal.NewOAuthConfig(auth.ADEndpointURL, auth.TenantID) if err != nil { return nil, err } @@ -53,7 +58,7 @@ func GetArmAuthorizerFromSdkAuth(auth SDKAuth) (*autorest.Authorizer, error) { auth.ARMEndpointURL = azure.PublicCloud.ResourceManagerEndpoint } - token, err := adal.NewServicePrincipalToken(*oauthconfig, auth.ClientID, auth.ClientSecret, auth.ARMEndpointURL) + token, err := adal.NewServicePrincipalToken(*oauthConfig, auth.ClientID, auth.ClientSecret, auth.ARMEndpointURL) if err != nil { return nil, err } @@ -62,11 +67,11 @@ func GetArmAuthorizerFromSdkAuth(auth SDKAuth) (*autorest.Authorizer, error) { var authorizer autorest.Authorizer authorizer = autorest.NewBearerAuthorizer(token) - return &authorizer, nil + return authorizer, nil } // GetArmAuthorizerFromSdkAuthJSON creats am ARM authorizer from the passed sdk auth file -func GetArmAuthorizerFromSdkAuthJSON(path string) (*autorest.Authorizer, error) { +func GetArmAuthorizerFromSdkAuthJSON(path string) (autorest.Authorizer, error) { var authorizer autorest.Authorizer // Manipulate the AZURE_AUTH_LOCATION var at runtime @@ -74,23 +79,23 @@ func GetArmAuthorizerFromSdkAuthJSON(path string) (*autorest.Authorizer, error) defer os.Unsetenv("AZURE_AUTH_LOCATION") authorizer, err := auth.NewAuthorizerFromFile(azure.PublicCloud.ResourceManagerEndpoint) - return &authorizer, err + return authorizer, err } // GetArmAuthorizerFromSdkAuthJSONString creates an ARM authorizer from the sdk auth credentials -func GetArmAuthorizerFromSdkAuthJSONString(credentials string) (*autorest.Authorizer, error) { +func GetArmAuthorizerFromSdkAuthJSONString(credentials string) (autorest.Authorizer, error) { var authorizer autorest.Authorizer // create a temporary file, as the sdk credentials need to be read from a file tmpFile, err := ioutil.TempFile(os.TempDir(), "azure-sdk-auth-") if err != nil { - return &authorizer, fmt.Errorf("Cannot create temporary sdk auth file: %s", err) + return authorizer, fmt.Errorf("Cannot create temporary sdk auth file: %s", err) } defer os.Remove(tmpFile.Name()) text := []byte(credentials) if _, err = tmpFile.Write(text); err != nil { - return &authorizer, fmt.Errorf("Failed to write to temporary sdk auth file: %s", err) + return authorizer, fmt.Errorf("Failed to write to temporary sdk auth file: %s", err) } tmpFile.Close() @@ -100,7 +105,7 @@ func GetArmAuthorizerFromSdkAuthJSONString(credentials string) (*autorest.Author authorizer, err = auth.NewAuthorizerFromFile(azure.PublicCloud.ResourceManagerEndpoint) - return &authorizer, err + return authorizer, err } // GetArmAuthorizerFromEnvironment creates an ARM authorizer from a MSI (AAD Pod Identity) @@ -215,27 +220,3 @@ func CreateDeploymentAtSubscriptionScope(ctx context.Context, deployClient resou return future.Result(deployClient) } - -func GetActiveSubscriptionFromCLI() (string, error) { - cliCmd := cli.GetAzureCLICommand() - cliCmd.Args = append(cliCmd.Args, "account", "show", "-o", "json") - - output, err := cliCmd.Output() - if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("Invoking Azure CLI failed with the following error: %s", ee.Stderr) - } - - return "", fmt.Errorf("Invoking Azure CLI failed with the following error: %s", err.Error()) - } - - var data struct { - SubscriptionID string `json:"id"` - } - err = json.Unmarshal(output, &data) - if err != nil { - return "", err - } - - return data.SubscriptionID, err -} diff --git a/pkg/github/actions/authenticate.go b/pkg/github/actions/authenticate.go index 7a9cd2f..3ce1f9a 100644 --- a/pkg/github/actions/authenticate.go +++ b/pkg/github/actions/authenticate.go @@ -13,12 +13,10 @@ import ( // Authenticate creates and azure authorizer func Authenticate(inputs github.Inputs) (autorest.Authorizer, error) { - var authorizer autorest.Authorizer - // Load authorizer from the service principal - authorizer, err := azure.GetArmAuthorizerFromCLI() + authorizer, err := azure.GetArmAuthorizerFromSdkAuth(inputs.Credentials) if err != nil { - return authorizer, err + return nil, err } return authorizer, nil diff --git a/pkg/github/actions/deploy.go b/pkg/github/actions/deploy.go index e152342..57fbc28 100644 --- a/pkg/github/actions/deploy.go +++ b/pkg/github/actions/deploy.go @@ -22,23 +22,13 @@ import ( // Deploy takes our inputs and initaite and // waits for completion of the arm template deployment func Deploy(ctx context.Context, inputs github.Inputs, authorizer autorest.Authorizer) (resources.DeploymentExtended, error) { - var subscriptionId = inputs.SubscriptionId var err error - // Try reading the subscription id from the cli if not set explicitly - if subscriptionId == "" { - subscriptionId, err = azure.GetActiveSubscriptionFromCLI() - logrus.Info(subscriptionId) - if err != nil { - return resources.DeploymentExtended{}, err - } - } - // Load the arm deployments client - deploymentsClient := azure.GetDeploymentsClient(subscriptionId, authorizer) - uuid := uuid.New().String() - logrus.Infof("Creating deployment %s with uuid %s -> %s-%s, mode: %s", inputs.DeploymentName, uuid, inputs.DeploymentName, uuid, inputs.DeploymentMode) - inputs.DeploymentName = fmt.Sprintf("%s-%s", inputs.DeploymentName, uuid) + deploymentsClient := azure.GetDeploymentsClient(inputs.Credentials.SubscriptionID, authorizer) + u := uuid.New().String() + logrus.Infof("Creating deployment %s with uuid %s -> %s-%s, mode: %s", inputs.DeploymentName, u, inputs.DeploymentName, u, inputs.DeploymentMode) + inputs.DeploymentName = fmt.Sprintf("%s-%s", inputs.DeploymentName, u) // Build our final parameters parameter := util.MergeParameters(inputs.Parameters, inputs.OverrideParameters) diff --git a/pkg/github/options.go b/pkg/github/options.go index 775e69b..20d6b24 100644 --- a/pkg/github/options.go +++ b/pkg/github/options.go @@ -14,6 +14,7 @@ import ( "github.com/caarlos0/env" "github.com/sirupsen/logrus" + "github.com/whiteducksoftware/azure-arm-action/pkg/azure" "github.com/whiteducksoftware/azure-arm-action/pkg/util" ) @@ -36,10 +37,10 @@ type GitHub struct { // Inputs represents our custom inputs for the action type Inputs struct { + Credentials azure.SDKAuth `env:"INPUT_CREDS"` Template template `env:"INPUT_TEMPLATELOCATION"` Parameters parameters `env:"INPUT_PARAMETERS"` OverrideParameters parameters `env:"INPUT_OVERRIDEPARAMETERS"` - SubscriptionId string `env:"INPUT_SUBSCRIPTIONID"` ResourceGroupName string `env:"INPUT_RESOURCEGROUPNAME"` DeploymentName string `env:"INPUT_DEPLOYMENTNAME"` DeploymentMode string `env:"INPUT_DEPLOYMENTMODE"` @@ -73,8 +74,13 @@ func LoadOptions() (*Options, error) { // custom type parser var customTypeParser = map[reflect.Type]env.ParserFunc{ - reflect.TypeOf(template{}): wrapReadJSON, - reflect.TypeOf(parameters{}): wrapReadParameters, + reflect.TypeOf(azure.SDKAuth{}): wrapGetServicePrincipal, + reflect.TypeOf(template{}): wrapReadJSON, + reflect.TypeOf(parameters{}): wrapReadParameters, +} + +func wrapGetServicePrincipal(v string) (interface{}, error) { + return azure.GetSdkAuthFromString(v) } func wrapReadJSON(v string) (interface{}, error) { diff --git a/version.go b/version.go deleted file mode 100644 index e5848e8..0000000 --- a/version.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "fmt" - -type Version struct { - GitSha string - GoVersion string - BuildTime string -} - -var ( - __Version__ Version - gitSha string - goVersion string - buildTime string -) - -func init() { - __Version__ = Version{ - GitSha: gitSha, - GoVersion: goVersion, - BuildTime: buildTime, - } -} - -func (ver Version) String() string { - return fmt.Sprintf("Git SHA: %s, Go Version: %s, Build Time: %s", ver.GitSha, ver.GoVersion, ver.BuildTime) -}