diff --git a/docker/Dockerfile b/docker/Dockerfile index 2868676..82a8f13 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,12 @@ +FROM golang:1.15 as tf-prepare-builder +WORKDIR /workspace + +COPY ./go-tf-prepare/go.mod ./go-tf-prepare/go.sum ./ +RUN go mod download +COPY ./go-tf-prepare/main.go main.go +COPY ./go-tf-prepare/pkg/ pkg/ +RUN GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o tf-prepare main.go + FROM alpine:3.12.1 ENV USER="tools" @@ -75,6 +84,9 @@ RUN /usr/src/install-scripts/helm.sh --version="v3.4.1" --sha="538f85b4b73ac6160 COPY install-scripts/cleanup.sh /usr/src/install-scripts/cleanup.sh RUN /usr/src/install-scripts/cleanup.sh +COPY --from=tf-prepare-builder /workspace/tf-prepare /usr/local/bin/tf-prepare +RUN chmod +x /usr/local/bin/tf-prepare + RUN rm -rf /tmp/install COPY opa-policies /opt/opa-policies diff --git a/docker/go-tf-prepare/go.mod b/docker/go-tf-prepare/go.mod new file mode 100644 index 0000000..3b37ad2 --- /dev/null +++ b/docker/go-tf-prepare/go.mod @@ -0,0 +1,18 @@ +module github.com/xenitab/github-actions/docker/go-tf-prepare + +go 1.15 + +require ( + github.com/Azure/azure-sdk-for-go v46.0.0+incompatible + github.com/Azure/azure-sdk-for-go/sdk/arm/keyvault/2019-09-01/armkeyvault v0.1.0 + github.com/Azure/azure-sdk-for-go/sdk/arm/resources/2020-06-01/armresources v0.1.0 + github.com/Azure/azure-sdk-for-go/sdk/arm/storage/2019-06-01/armstorage v0.1.0 + github.com/Azure/azure-sdk-for-go/sdk/armcore v0.5.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v0.13.4 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.7.0 + github.com/Azure/azure-sdk-for-go/sdk/to v0.1.2 + github.com/go-logr/logr v0.3.0 + github.com/go-logr/stdr v0.3.0 + github.com/jongio/azidext/go/azidext v0.1.0 + github.com/urfave/cli/v2 v2.3.0 +) diff --git a/docker/go-tf-prepare/go.sum b/docker/go-tf-prepare/go.sum new file mode 100644 index 0000000..8cf4017 --- /dev/null +++ b/docker/go-tf-prepare/go.sum @@ -0,0 +1,90 @@ +github.com/Azure/azure-sdk-for-go v46.0.0+incompatible h1:4qlEOCDcDQZTGczYGzbGYCdJfVpZLIs8AEo5+MoXBPw= +github.com/Azure/azure-sdk-for-go v46.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/arm/keyvault/2019-09-01/armkeyvault v0.1.0 h1:4QOAacPzIU1dw03tLFRA64r6+EuzXnKUUR2eeMwes8Y= +github.com/Azure/azure-sdk-for-go/sdk/arm/keyvault/2019-09-01/armkeyvault v0.1.0/go.mod h1:3p+zC+iBXbnp1014TnjW42FHi594AKtpzIeJqbCGPks= +github.com/Azure/azure-sdk-for-go/sdk/arm/resources/2020-06-01/armresources v0.1.0 h1:lukRorBGhLmNospZsyC4ZBmKFSPjFm0EP2zhnURENSQ= +github.com/Azure/azure-sdk-for-go/sdk/arm/resources/2020-06-01/armresources v0.1.0/go.mod h1:BLfFIsqo/B2o7zUM/QbeTe9uu4FMpBjARi20XbmrOq8= +github.com/Azure/azure-sdk-for-go/sdk/arm/storage/2019-06-01/armstorage v0.1.0 h1:zMV0Tb4H7GfW0jNwH6ZO2hZt7O/gDMMih4248s8Wdg4= +github.com/Azure/azure-sdk-for-go/sdk/arm/storage/2019-06-01/armstorage v0.1.0/go.mod h1:tUy2yqX9i49NGIoOk7wSY9ULRlFsouD1oInDK+lDC1A= +github.com/Azure/azure-sdk-for-go/sdk/armcore v0.5.1 h1:VGqJCzGuqWjTvMspLSaWHiP1vUOA0lAdjoPXjhltfQQ= +github.com/Azure/azure-sdk-for-go/sdk/armcore v0.5.1/go.mod h1:+sBBoB6bha/qi//hAzfa4dd8EWRKBNYzSJjb9IccpXM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.10.0/go.mod h1:R+GJZ0mj7yxXtTENNLTzwkwro5zWzrEiZOdpIiN7Ypc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.13.1/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.13.4 h1:7MfvHEWKfjZSKQNWERlXpHwCRoceEuQef/fB8CWmnQA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.13.4/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.2.0/go.mod h1:/XqWZ+BVfDwHnN6x+Ns+VH2Le0x0Yhks6I2DHkIyGGo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.6.0/go.mod h1:BfjVb0eeNKsOveaOBnAgUv6nSq5hwScOz7mCm9lqUx8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.7.0 h1:qD6HKtTRI0AMrXB+G0TJXxbB7c1gnG8zOROK8eKsbI0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.7.0/go.mod h1:+5MTVs48VhHs2R1WpIFv7wy7vJ/acthpwlvcd2eN/Ok= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.3.0/go.mod h1:Q+TCQnSr+clUU0JU+xrHZ3slYCxw17AOFdvWFpQXjAY= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0 h1:HG1ggl8L3ZkV/Ydanf7lKr5kkhhPGCpWdnr1J6v7cO4= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM= +github.com/Azure/azure-sdk-for-go/sdk/to v0.1.1/go.mod h1:UL/d4lvWAzSJUuX+19uKdN0ktyjoOyQhgY+HWNgtIYI= +github.com/Azure/azure-sdk-for-go/sdk/to v0.1.2 h1:TZTVOb/ce7nCmOZYga9+ELtPPVVFG2Px4s/w5OycYS0= +github.com/Azure/azure-sdk-for-go/sdk/to v0.1.2/go.mod h1:UL/d4lvWAzSJUuX+19uKdN0ktyjoOyQhgY+HWNgtIYI= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.4 h1:iWJqGEvip7mjibEqC/srXNdo+4wLEPiwlP/7dZLtoPc= +github.com/Azure/go-autorest/autorest v0.11.4/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0 h1:SigMbuFNuKgc1xcGhaeapbh+8fgsu+GxgDRFyg7f5lM= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0 h1:z20OWOSG5aCye0HEkDp6TPmP17ZcfeMxPi6HnSALa8c= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.0 h1:3I9AAI63HfcLtphd9g39ruUwRI+Ca+z/f36KHPFRUss= +github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs= +github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/stdr v0.3.0 h1:GzFt/sOHlPMqsh46UTAaIDWPFq3DdNGcF4rwMjhTBVo= +github.com/go-logr/stdr v0.3.0/go.mod h1:NO1vneyJDqKVgJYnxhwXWWmQPOvNM391IG3H8ql3jiA= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jongio/azidext/go/azidext v0.1.0 h1:FlT+pmODYf82hqyQtE5C/Fajdt64wos88k2d7yhnhHk= +github.com/jongio/azidext/go/azidext v0.1.0/go.mod h1:v7DP8YodvY0fd6An/6j1A6OlU8SxPH1L7pjWcE/svik= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/docker/go-tf-prepare/main.go b/docker/go-tf-prepare/main.go new file mode 100644 index 0000000..381f24d --- /dev/null +++ b/docker/go-tf-prepare/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + stdlog "log" + "os" + + "github.com/go-logr/logr" + "github.com/go-logr/stdr" + "github.com/urfave/cli/v2" + "github.com/xenitab/github-actions/docker/go-tf-prepare/pkg/azure" +) + +func main() { + stdr.SetVerbosity(1) + log := stdr.New(stdlog.New(os.Stderr, "", stdlog.LstdFlags|stdlog.Lshortfile)) + log = log.WithName("tf-prepare") + + ctx := logr.NewContext(context.Background(), log) + + app := &cli.App{ + Commands: []*cli.Command{ + { + Name: "azure", + Usage: "Terraform prepare for Azure", + Flags: azure.Flags(), + Action: func(cli *cli.Context) error { + err := azure.Action(ctx, cli) + if err != nil { + return err + } + return nil + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Error(err, "CLI execution failed") + os.Exit(1) + } + + os.Exit(0) +} diff --git a/docker/go-tf-prepare/pkg/azure/azure.go b/docker/go-tf-prepare/pkg/azure/azure.go new file mode 100644 index 0000000..08507c7 --- /dev/null +++ b/docker/go-tf-prepare/pkg/azure/azure.go @@ -0,0 +1,435 @@ +package azure + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/arm/keyvault/2019-09-01/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/arm/resources/2020-06-01/armresources" + "github.com/Azure/azure-sdk-for-go/sdk/arm/storage/2019-06-01/armstorage" + "github.com/Azure/azure-sdk-for-go/sdk/armcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/to" + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-09-01/locks" + "github.com/go-logr/logr" + "github.com/jongio/azidext/go/azidext" +) + +// CreateResourceGroup creates Azure Resource Group (if it doesn't exist) or returns error +func CreateResourceGroup(ctx context.Context, resourceGroupName, resourceGroupLocation, subscriptionID string) error { + log := logr.FromContext(ctx) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + + client := armresources.NewResourceGroupsClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + resourceGroupExists, err := client.CheckExistence(ctx, resourceGroupName, &armresources.ResourceGroupsCheckExistenceOptions{}) + if err != nil { + log.Error(err, "client.CheckExistence") + return err + } + if !resourceGroupExists.Success { + _, err = client.CreateOrUpdate(ctx, resourceGroupName, armresources.ResourceGroup{ + Location: to.StringPtr(resourceGroupLocation), + }, nil) + if err != nil { + log.Error(err, "client.CreateOrUpdate") + return err + } + + log.Info("Azure Resource Group created", "resourceGroupName", resourceGroupName) + return nil + } + + log.Info("Azure Resource Group already exists", "resourceGroupName", resourceGroupName) + return nil +} + +// CreateStorageAccount creates Azure Storage Account (if it doesn't exist) or returns error +func CreateStorageAccount(ctx context.Context, resourceGroupName, resourceGroupLocation, storageAccountName, subscriptionID string) error { + log := logr.FromContext(ctx) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + client := armstorage.NewStorageAccountsClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + _, err = client.GetProperties(ctx, resourceGroupName, storageAccountName, nil) + + if err == nil { + log.Info("Azure Storage Account already exists", "storageAccountName", storageAccountName) + return nil + } + + if err != nil && strings.Contains(err.Error(), "ResourceNotFound") { + res, err := client.CheckNameAvailability( + ctx, + armstorage.StorageAccountCheckNameAvailabilityParameters{ + Name: to.StringPtr(storageAccountName), + Type: to.StringPtr("Microsoft.Storage/storageAccounts"), + }, + nil) + + if err != nil { + log.Error(err, "client.CheckNameAvailability") + return err + } + + if !*res.CheckNameAvailabilityResult.NameAvailable { + log.Error(err, "client.CheckNameAvailability: Azure Storage Account Name not available", "storageAccountName", storageAccountName) + return err + } + + poller, err := client.BeginCreate( + ctx, + resourceGroupName, + storageAccountName, + armstorage.StorageAccountCreateParameters{ + SKU: &armstorage.SKU{ + Name: armstorage.SKUNameStandardGrs.ToPtr(), + Tier: armstorage.SKUTierStandard.ToPtr(), + }, + Kind: armstorage.KindStorageV2.ToPtr(), + Location: to.StringPtr(resourceGroupLocation), + Properties: &armstorage.StorageAccountPropertiesCreateParameters{ + AccessTier: armstorage.AccessTierHot.ToPtr(), + AllowBlobPublicAccess: to.BoolPtr(false), + MinimumTLSVersion: armstorage.MinimumTLSVersionTLS12.ToPtr(), + }, + }, nil) + + if err != nil { + log.Error(err, "client.BeginCreate") + return err + } + + _, err = poller.PollUntilDone(ctx, 30*time.Second) + if err != nil { + log.Error(err, "poller.PollUntilDone") + return err + } + + log.Info("Azure Storage Account created", "storageAccountName", storageAccountName) + return nil + } + + log.Error(err, "client.GetProperties") + return err +} + +// CreateStorageAccountContainer creates Storage Account Container (if it doesn't exist) or returns error +func CreateStorageAccountContainer(ctx context.Context, resourceGroupName, storageAccountName, storageAccountContainer, subscriptionID string) error { + log := logr.FromContext(ctx) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + client := armstorage.NewBlobContainersClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + _, err = client.Get( + ctx, + resourceGroupName, + storageAccountName, + storageAccountContainer, nil) + + if err == nil { + log.Info("Azure Storage Account Container already exists", "storageAccountContainer", storageAccountContainer) + return nil + } + + if err != nil && strings.Contains(err.Error(), "The specified container does not exist") { + _, err := client.Create( + ctx, + resourceGroupName, + storageAccountName, + storageAccountContainer, + armstorage.BlobContainer{}, nil) + + if err != nil { + log.Error(err, "client.Create") + return err + } + + log.Info("Azure Storage Account Container created", "storageAccountContainer", storageAccountContainer) + return nil + } + + log.Error(err, "armstorage.NewBlobContainersClient") + return err +} + +// CreateKeyVault creates Azure Key Vault (if it doesn't exist) or returns error +func CreateKeyVault(ctx context.Context, resourceGroupName, resourceGroupLocation, keyVaultName, subscriptionID, tenantID string) error { + log := logr.FromContext(ctx) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + client := armkeyvault.NewVaultsClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + + _, err = client.Get(ctx, resourceGroupName, keyVaultName, nil) + if err == nil { + log.Info("Azure KeyVault already exists", "keyVaultName", keyVaultName) + return nil + } + + if err != nil && strings.Contains(err.Error(), "ResourceNotFound") { + keyVaultNameAvailable, err := client.CheckNameAvailability(ctx, armkeyvault.VaultCheckNameAvailabilityParameters{Name: to.StringPtr(keyVaultName), Type: to.StringPtr("Microsoft.KeyVault/vaults")}, nil) + if err != nil { + log.Error(err, "client.CheckNameAvailability") + return err + } + + if !*keyVaultNameAvailable.CheckNameAvailabilityResult.NameAvailable { + log.Error(err, "client.CheckNameAvailability: Azure KeyVault Name not available", "keyVaultName", keyVaultName) + return err + } + + poll, err := client.BeginCreateOrUpdate( + ctx, + resourceGroupName, + keyVaultName, + armkeyvault.VaultCreateOrUpdateParameters{ + Location: to.StringPtr(resourceGroupLocation), + Properties: &armkeyvault.VaultProperties{ + TenantID: to.StringPtr(tenantID), + SKU: &armkeyvault.SKU{ + Family: armkeyvault.SKUFamilyA.ToPtr(), + Name: armkeyvault.SKUNameStandard.ToPtr(), + }, + AccessPolicies: &[]armkeyvault.AccessPolicyEntry{}, + }, + }, nil) + if err != nil { + log.Error(err, "client.BeginCreateOrUpdate") + return err + } + _, err = poll.PollUntilDone(ctx, 5*time.Second) + if err != nil { + log.Error(err, "poll.PollUntilDone") + return err + } + + log.Info("Azure KeyVault created", "keyVaultName", keyVaultName) + return nil + } + + return fmt.Errorf("Failed Azure/CreateKeyVault/client.Get: %v", err) +} + +// CreateKeyVaultAccessPolicy creates Azure Key Vault Access Policy (if it doesn't exist) or returns error +func CreateKeyVaultAccessPolicy(ctx context.Context, resourceGroupName, resourceGroupLocation, keyVaultName, subscriptionID, tenantID, servicePrincipalObjectID string) error { + log := logr.FromContext(ctx) + + var currentUserObjectID string + if servicePrincipalObjectID == "" { + var err error + currentUserObjectID, err = getCurrentUserObjectID(ctx, tenantID) + if err != nil { + log.Error(err, "getCurrentUserObjectID") + return err + } + } + if servicePrincipalObjectID != "" { + currentUserObjectID = servicePrincipalObjectID + } + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + + client := armkeyvault.NewVaultsClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + + keyPermissions := armkeyvault.Permissions{ + Keys: &[]armkeyvault.KeyPermissions{ + armkeyvault.KeyPermissionsUpdate, + armkeyvault.KeyPermissionsCreate, + armkeyvault.KeyPermissionsGet, + armkeyvault.KeyPermissionsList, + armkeyvault.KeyPermissionsEncrypt, + armkeyvault.KeyPermissionsDecrypt, + }, + } + + accessPolicies := []armkeyvault.AccessPolicyEntry{ + { + TenantID: &tenantID, + ObjectID: ¤tUserObjectID, + Permissions: &keyPermissions, + }, + } + + properties := armkeyvault.VaultAccessPolicyProperties{AccessPolicies: &accessPolicies} + parameters := armkeyvault.VaultAccessPolicyParameters{Properties: &properties} + options := armkeyvault.VaultsUpdateAccessPolicyOptions{} + + kv, err := client.Get(ctx, resourceGroupName, keyVaultName, nil) + if err != nil { + log.Error(err, "client.Get") + return err + } + + // Loop through all access policies + for _, accessPolicy := range *kv.Vault.Properties.AccessPolicies { + // Check if the current object id for the access policy is the same as the current user object id + if *accessPolicy.ObjectID == currentUserObjectID { + // Check if the Key Permissions in the access policy are the same as the required Key Permissions + if keyPermissionsEqual(*accessPolicy.Permissions.Keys, *keyPermissions.Keys) { + // If the correct Key Permissions already exists, return early + log.Info("Azure KeyVault Access Policy already correct", "currentUserObjectID", currentUserObjectID) + return nil + } + } + } + + _, err = client.UpdateAccessPolicy(ctx, resourceGroupName, keyVaultName, armkeyvault.AccessPolicyUpdateKindAdd, parameters, &options) + if err != nil { + log.Error(err, "client.UpdateAccessPolicy") + return err + } + + log.Info("Azure KeyVault Access Policy created or updated", "currentUserObjectID", currentUserObjectID) + + return nil +} + +// CreateKeyVaultKey creates Azure Key Vault Key (if it doesn't exist) or returns error +func CreateKeyVaultKey(ctx context.Context, resourceGroupName, keyVaultName, keyName, subscriptionID string) error { + log := logr.FromContext(ctx) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Error(err, "azidentity.NewDefaultAzureCredential") + return err + } + + client := armkeyvault.NewKeysClient(armcore.NewDefaultConnection(cred, nil), subscriptionID) + + _, err = client.Get(ctx, resourceGroupName, keyVaultName, keyName, nil) + if err == nil { + log.Info("Azure KeyVault Key already exists", "keyName", keyName) + return nil + } + + _, err = client.CreateIfNotExist( + ctx, + resourceGroupName, + keyVaultName, + keyName, + armkeyvault.KeyCreateParameters{ + Properties: &armkeyvault.KeyProperties{ + Attributes: &armkeyvault.Attributes{ + Enabled: to.BoolPtr(true), + }, + KeySize: to.Int32Ptr(2048), + KeyOps: &[]armkeyvault.JSONWebKeyOperation{ + armkeyvault.JSONWebKeyOperationEncrypt, + armkeyvault.JSONWebKeyOperationDecrypt, + }, + Kty: armkeyvault.JSONWebKeyTypeRsa.ToPtr(), + }}, nil) + if err != nil { + log.Error(err, "armkeyvault.NewKeysClient") + return err + } + + log.Info("Azure KeyVault Key created", "keyName", keyName) + return nil +} + +// CreateResourceLock creates Azure Resource Lock (if it doesn't exist) or return error +func CreateResourceLock(ctx context.Context, resourceGroupName, resourceProviderNamespace, parentResourcePath, resourceType, resourceName, lockName, subscriptionID string) error { + log := logr.FromContext(ctx) + + client := locks.NewManagementLocksClient(subscriptionID) + + authorizer, err := azidext.NewDefaultAzureCredentialAdapter(nil) + if err != nil { + log.Error(err, "azidext.NewDefaultAzureCredentialAdapter") + return err + } + + client.Authorizer = authorizer + + _, err = client.GetAtResourceLevel(ctx, resourceGroupName, resourceProviderNamespace, parentResourcePath, resourceType, resourceName, lockName) + if err == nil { + log.Info("Azure Resource Lock already exists", "resourceGroupName", resourceGroupName, "resourceProviderNamespace", resourceProviderNamespace, "resourceType", resourceType, "resourceName", resourceName) + return nil + } + + if err != nil && strings.Contains(err.Error(), "LockNotFound") { + _, err = client.CreateOrUpdateAtResourceLevel(ctx, resourceGroupName, resourceProviderNamespace, parentResourcePath, resourceType, resourceName, lockName, locks.ManagementLockObject{ManagementLockProperties: &locks.ManagementLockProperties{Level: "CanNotDelete", Notes: to.StringPtr("CanNotDelete")}}) + if err != nil { + log.Error(err, "client.CreateOrUpdateAtResourceLevel") + return err + } + + log.Info("Azure Resource Lock created", "resourceGroupName", resourceGroupName, "resourceProviderNamespace", resourceProviderNamespace, "resourceType", resourceType, "resourceName", resourceName) + return nil + } + + log.Error(err, "client.GetAtResourceLevel") + return err +} + +func getCurrentUserObjectID(ctx context.Context, tenantID string) (string, error) { + log := logr.FromContext(ctx) + + client := graphrbac.NewSignedInUserClient(tenantID) + + defaultCredential := azidentity.DefaultAzureCredentialOptions{} + tokenRequestOptions := azcore.TokenRequestOptions{Scopes: []string{"https://graph.windows.net/.default"}} + authenticationPolicy := azcore.AuthenticationPolicyOptions{Options: tokenRequestOptions} + credentialOptions := azidext.DefaultAzureCredentialOptions{DefaultCredential: &defaultCredential, AuthenticationPolicy: &authenticationPolicy} + authorizer, err := azidext.NewDefaultAzureCredentialAdapter(&credentialOptions) + if err != nil { + log.Error(err, "azidext.NewDefaultAzureCredentialAdapter") + return "", err + } + + client.Authorizer = authorizer + + currentUser, err := client.Get(ctx) + if err != nil { + log.Error(err, "client.Get") + return "", err + } + + return *currentUser.ObjectID, nil +} + +func keyPermissionsEqual(a, b []armkeyvault.KeyPermissions) bool { + if (a == nil) != (b == nil) { + return false + } + + if len(a) != len(b) { + return false + } + +OUTER: + for _, i := range a { + for _, j := range b { + // a may have the first letters uppercase while b always have them lowercase + if strings.ToLower(string(i)) == strings.ToLower(string(j)) { + continue OUTER + } + } + return false + } + + return true +} diff --git a/docker/go-tf-prepare/pkg/azure/azure_cli.go b/docker/go-tf-prepare/pkg/azure/azure_cli.go new file mode 100644 index 0000000..f170599 --- /dev/null +++ b/docker/go-tf-prepare/pkg/azure/azure_cli.go @@ -0,0 +1,134 @@ +package azure + +import ( + "context" + + "github.com/urfave/cli/v2" +) + +// Action executes the Azure action +func Action(ctx context.Context, cli *cli.Context) error { + servicePrincipalObjectID := cli.String("service-principal-object-id") + subscriptionID := cli.String("subscription-id") + tenantID := cli.String("tenant-id") + resourceGroupName := cli.String("resource-group-name") + resourceGroupLocation := cli.String("resource-group-location") + storageAccountName := cli.String("storage-account-name") + storageAccountContainer := cli.String("storage-account-container") + keyVaultName := cli.String("keyvault-name") + keyVaultKeyName := cli.String("keyvault-key-name") + resourceLocks := cli.Bool("resource-locks") + + err := CreateResourceGroup(ctx, resourceGroupName, resourceGroupLocation, subscriptionID) + if err != nil { + return err + } + + err = CreateStorageAccount(ctx, resourceGroupName, resourceGroupLocation, storageAccountName, subscriptionID) + if err != nil { + return err + } + + if resourceLocks { + err = CreateResourceLock(ctx, resourceGroupName, "Microsoft.Storage", "", "storageAccounts", storageAccountName, "DoNotDelete", subscriptionID) + if err != nil { + return err + } + } + + err = CreateStorageAccountContainer(ctx, resourceGroupName, storageAccountName, storageAccountContainer, subscriptionID) + if err != nil { + return err + } + + err = CreateKeyVault(ctx, resourceGroupName, resourceGroupLocation, keyVaultName, subscriptionID, tenantID) + if err != nil { + return err + } + + if resourceLocks { + err = CreateResourceLock(ctx, resourceGroupName, "Microsoft.KeyVault", "", "vaults", keyVaultName, "DoNotDelete", subscriptionID) + if err != nil { + return err + } + } + + err = CreateKeyVaultAccessPolicy(ctx, resourceGroupName, resourceGroupLocation, keyVaultName, subscriptionID, tenantID, servicePrincipalObjectID) + if err != nil { + return err + } + + err = CreateKeyVaultKey(ctx, resourceGroupName, keyVaultName, keyVaultKeyName, subscriptionID) + if err != nil { + return err + } + + return nil +} + +// Flags returns the cli flags for Azure +func Flags() []cli.Flag { + flags := []cli.Flag{ + &cli.StringFlag{ + Name: "service-principal-object-id", + Usage: "Service Principal Object ID", + Required: false, + EnvVars: []string{"AZURE_SERVICE_PRINCIPAL_OBJECT_ID"}, + }, + &cli.StringFlag{ + Name: "subscription-id", + Usage: "Azure Subscription ID", + Required: true, + EnvVars: []string{"AZURE_SUBSCRIPTION_ID"}, + }, + &cli.StringFlag{ + Name: "tenant-id", + Usage: "Azure Tenant ID", + Required: true, + EnvVars: []string{"AZURE_TENANT_ID"}, + }, + &cli.StringFlag{ + Name: "resource-group-name", + Usage: "Azure Resource Group Name", + Required: true, + EnvVars: []string{"AZURE_RESOURCE_GROUP_NAME"}, + }, + &cli.StringFlag{ + Name: "resource-group-location", + Usage: "Azure Resource Group Location", + Required: true, + EnvVars: []string{"AZURE_RESOURCE_GROUP_LOCATION"}, + }, + &cli.StringFlag{ + Name: "storage-account-name", + Usage: "Azure Storage Account Name", + Required: true, + EnvVars: []string{"AZURE_STORAGE_ACCOUNT_NAME"}, + }, + &cli.StringFlag{ + Name: "storage-account-container", + Usage: "Azure Storage Account Container", + Required: true, + EnvVars: []string{"AZURE_STORAGE_ACCOUNT_CONTAINER"}, + }, + &cli.StringFlag{ + Name: "keyvault-name", + Usage: "Azure KeyVault Name", + Required: true, + EnvVars: []string{"AZURE_KEYVAULT_NAME"}, + }, + &cli.StringFlag{ + Name: "keyvault-key-name", + Usage: "Azure KeyVault Key Name", + Required: true, + EnvVars: []string{"AZURE_KEYVAULT_KEY_NAME"}, + }, + &cli.BoolFlag{ + Name: "resource-locks", + Usage: "Should Azure Resource Locks be used?", + Value: true, + EnvVars: []string{"AZURE_RESOURCE_LOCKS"}, + }, + } + return flags +} diff --git a/docker/terraform.sh b/docker/terraform.sh index 70a12f0..4c7c416 100755 --- a/docker/terraform.sh +++ b/docker/terraform.sh @@ -21,52 +21,21 @@ if [ -z "${OPA_BLAST_RADIUS}" ]; then OPA_BLAST_RADIUS=50 fi -set_azure_keyvault_permissions() { - echo "Assigning permissions to Azure KeyVault ${BACKEND_KV}" - AZ_ACCOUNT_TYPE="$(az account show --query user.type --output tsv)" - if [[ "${AZ_ACCOUNT_TYPE}" = "user" ]]; then - AZ_USER_OBJECT_ID="$(az ad signed-in-user show --query objectId --output tsv)" - az keyvault set-policy --name ${BACKEND_KV} --resource-group ${BACKEND_RG} --object-id ${AZ_USER_OBJECT_ID} --key-permissions create get list encrypt decrypt 1>/dev/null - elif [[ "${AZ_ACCOUNT_TYPE}" = "servicePrincipal" ]]; then - AZ_SPN="$(az account show --query user.name --output tsv)" - az keyvault set-policy --name ${BACKEND_KV} --resource-group ${BACKEND_RG} --spn ${AZ_SPN} --key-permissions create get list encrypt decrypt 1>/dev/null - fi -} - prepare () { - if [ $(az group exists --name ${BACKEND_RG}) = false ]; then - echo "INFO: Creating resource group ${BACKEND_RG} in location ${RG_LOCATION_LONG}" - az group create --name ${BACKEND_RG} --location ${RG_LOCATION_LONG} - fi - - if ! $(az storage account show --resource-group ${BACKEND_RG} --name ${BACKEND_NAME} --output none); then - echo "Creating Azure Storage Account ${BACKEND_NAME} in location ${RG_LOCATION_LONG} / resource group ${BACKEND_RG}" - az storage account create --resource-group ${BACKEND_RG} --name ${BACKEND_NAME} 1>/dev/null - fi - - if ! $(az storage container show --account-name ${BACKEND_NAME} --name ${CONTAINER_NAME} --output none); then - echo "Creating Azure Storage Container ${CONTAINER_NAME} in Storage Account ${BACKEND_NAME}" - az storage container create --account-name ${BACKEND_NAME} --name ${CONTAINER_NAME} 1>/dev/null - fi - - if ! $(az keyvault show --name ${BACKEND_KV} --output none); then - echo "Creating Azure KeyVault ${BACKEND_KV} in location ${RG_LOCATION_LONG} / resource group ${BACKEND_RG}" - az keyvault create --name ${BACKEND_KV} --resource-group ${BACKEND_RG} --location ${RG_LOCATION_LONG} 1>/dev/null - fi - - set +e - KEYVAULT_KEY_TEST="$(az keyvault key show --vault-name ${BACKEND_KV} --name ${BACKEND_KV_KEY} --output none 2>&1)" - if echo ${KEYVAULT_KEY_TEST} | grep KeyNotFound; then - echo "Creating Azure KeyVault key in ${BACKEND_KV}" - az keyvault key create --name ${BACKEND_KV_KEY} --vault-name ${BACKEND_KV} --protection software --ops encrypt decrypt 1>/dev/null - set_azure_keyvault_permissions - elif echo ${KEYVAULT_KEY_TEST} | grep "does not have keys get permission on key vault"; then - set_azure_keyvault_permissions + AZ_ACCOUNT_TYPE="$(az account show --query user.type --output tsv)" + if [[ "${AZ_ACCOUNT_TYPE}" = "servicePrincipal" ]]; then + export AZURE_SERVICE_PRINCIPAL_OBJECT_ID="$(az account show --query user.name --output tsv)" fi - set -e - - az lock create --name DoNotDelete --resource-group ${BACKEND_RG} --lock-type CanNotDelete --resource-type Microsoft.Storage/storageAccounts --resource ${BACKEND_NAME} 1>/dev/null - az lock create --name DoNotDelete --resource-group ${BACKEND_RG} --lock-type CanNotDelete --resource-type Microsoft.KeyVault/vaults --resource ${BACKEND_KV} 1>/dev/null + export AZURE_SUBSCRIPTION_ID=$(az account show --output tsv --query id) + export AZURE_TENANT_ID=$(az account show --output tsv --query tenantId) + export AZURE_RESOURCE_GROUP_NAME="${BACKEND_RG}" + export AZURE_RESOURCE_GROUP_LOCATION="${RG_LOCATION_LONG}" + export AZURE_STORAGE_ACCOUNT_NAME="${BACKEND_NAME}" + export AZURE_STORAGE_ACCOUNT_CONTAINER="${CONTAINER_NAME}" + export AZURE_KEYVAULT_NAME="${BACKEND_KV}" + export AZURE_KEYVAULT_KEY_NAME="${BACKEND_KV_KEY}" + export AZURE_RESOURCE_LOCKS="${AZURE_RESOURCE_LOCKS:-true}" + tf-prepare azure } plan () {