From 1dbf98fd9c8565dbdf08d57a38025a16ff67f52c Mon Sep 17 00:00:00 2001 From: sgargula Date: Mon, 12 Feb 2018 16:41:59 +0100 Subject: [PATCH 1/9] file format --- cliparser/cliparser.go | 30 +++++------------------------- converter/converter.go | 37 +++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/cliparser/cliparser.go b/cliparser/cliparser.go index eaac68c..3813d70 100644 --- a/cliparser/cliparser.go +++ b/cliparser/cliparser.go @@ -23,7 +23,7 @@ import ( "github.com/Appliscale/perun/logger" "github.com/Appliscale/perun/utilities" "gopkg.in/alecthomas/kingpin.v2" - "strings" + //"strings" ) var ValidateMode = "validate" @@ -33,14 +33,10 @@ var ConfigureMode = "configure" var CreateStackMode = "create-stack" var DestroyStackMode = "delete-stack" -const JSON = "json" -const YAML = "yaml" - type CliArguments struct { Mode *string TemplatePath *string OutputFilePath *string - OutputFileFormat *string ConfigurationPath *string Quiet *bool Yes *bool @@ -54,10 +50,6 @@ type CliArguments struct { PrettyPrint *bool } -func availableFormats() []string { - return []string{JSON, YAML} -} - // Get and validate CLI arguments. Returns error if validation fails. func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { var ( @@ -79,11 +71,10 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { offlineValidate = app.Command(OfflineValidateMode, "Offline Template Validation") offlineValidateTemplate = offlineValidate.Arg("template", "A path to the template file.").Required().String() - convert = app.Command(ConvertMode, "Convertion between JSON and YAML of template files") - convertTemplate = convert.Arg("template", "A path to the template file.").Required().String() - convertOutputFile = convert.Arg("output", "A path where converted file will be saved.").Required().String() - convertOutputFormat = convert.Arg("format", "Output format: "+strings.ToUpper(JSON)+" | "+strings.ToUpper(YAML)+".").HintAction(availableFormats).Required().String() - prettyPrint = convert.Flag("pretty-print", "Pretty printing JSON").Bool() + convert = app.Command(ConvertMode, "Convertion between JSON and YAML of template files") + convertTemplate = convert.Arg("template", "A path to the template file.").Required().String() + convertOutputFile = convert.Arg("output", "A path where converted file will be saved.").Required().String() + prettyPrint = convert.Flag("pretty-print", "Pretty printing JSON").Bool() configure = app.Command(ConfigureMode, "Create your own configuration mode") @@ -113,7 +104,6 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { cliArguments.Mode = &ConvertMode cliArguments.TemplatePath = convertTemplate cliArguments.OutputFilePath = convertOutputFile - cliArguments.OutputFileFormat = convertOutputFormat cliArguments.PrettyPrint = prettyPrint // configure @@ -121,7 +111,6 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { cliArguments.Mode = &ConfigureMode // create Stack - case createStack.FullCommand(): cliArguments.Mode = &CreateStackMode cliArguments.Stack = createStackName @@ -158,14 +147,5 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { return } - if *cliArguments.Mode == ConvertMode { - *cliArguments.OutputFileFormat = strings.ToLower(*cliArguments.OutputFileFormat) - if *cliArguments.OutputFileFormat != JSON && *cliArguments.OutputFileFormat != YAML { - err = errors.New("Invalid output file format. Use JSON or YAML") - return - } - - } - return } diff --git a/converter/converter.go b/converter/converter.go index 4aa4fec..164a5f7 100644 --- a/converter/converter.go +++ b/converter/converter.go @@ -21,7 +21,6 @@ package converter import ( "encoding/json" "errors" - "github.com/Appliscale/perun/cliparser" "github.com/Appliscale/perun/context" "github.com/Appliscale/perun/logger" "github.com/asaskevich/govalidator" @@ -37,17 +36,16 @@ func Convert(context *context.Context) error { if err != nil { return err } + format := chooseformat(rawTemplate) + var outputTemplate []byte - if *context.CliArguments.OutputFileFormat == cliparser.YAML { - outputTemplate, err := toYAML(rawTemplate) + if format == "JSON" { + outputTemplate, err = toYAML(rawTemplate) if err != nil { return err } saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) - } - - if *context.CliArguments.OutputFileFormat == cliparser.JSON { - var outputTemplate []byte + } else if format == "YAML" { if *context.CliArguments.PrettyPrint == false { outputTemplate, err = yamlToJSON(rawTemplate) } else if *context.CliArguments.PrettyPrint == true { @@ -56,11 +54,13 @@ func Convert(context *context.Context) error { if err != nil { return err } - - err = saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) - if err != nil { - return err - } + } else { + context.Logger.Always(format) + return nil + } + err = saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) + if err != nil { + return err } return nil @@ -112,3 +112,16 @@ func saveToFile(template []byte, path string, logger *logger.Logger) error { return nil } + +func chooseformat(rawTemplate []byte) (format string) { + + _, errorYAML := toYAML(rawTemplate) + _, errorJSON := yamlToJSON(rawTemplate) + + if errorYAML == nil { + return "JSON" + } else if errorJSON == nil { + return "YAML" + } + return "Invalid output file format. Use JSON or YAML" +} From de3a4e3a984ea1e7bfc739e81677d67b8e9debd7 Mon Sep 17 00:00:00 2001 From: sgargula Date: Tue, 13 Feb 2018 09:47:45 +0100 Subject: [PATCH 2/9] auto-detection of input file format --- cliparser/cliparser.go | 1 - cliparser/cliparser_test.go | 5 ----- converter/converter.go | 6 +++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/cliparser/cliparser.go b/cliparser/cliparser.go index 3813d70..5f3094d 100644 --- a/cliparser/cliparser.go +++ b/cliparser/cliparser.go @@ -23,7 +23,6 @@ import ( "github.com/Appliscale/perun/logger" "github.com/Appliscale/perun/utilities" "gopkg.in/alecthomas/kingpin.v2" - //"strings" ) var ValidateMode = "validate" diff --git a/cliparser/cliparser_test.go b/cliparser/cliparser_test.go index 8d1c84f..ce6afd6 100644 --- a/cliparser/cliparser_test.go +++ b/cliparser/cliparser_test.go @@ -21,11 +21,6 @@ import ( "testing" ) -func TestInvalidOutputFormatInConvertMode(t *testing.T) { - assert.Equal(t, "Invalid output file format. Use JSON or YAML", - parseCliArguments([]string{"cmd", "convert", "some_path", "some_path", "wrong_format"}).Error()) -} - func TestInvalidVerbosity(t *testing.T) { assert.Equal(t, "You specified invalid value for --verbosity flag", parseCliArguments([]string{"cmd", "validate", "some_path", "--verbosity=TEST"}).Error()) diff --git a/converter/converter.go b/converter/converter.go index 164a5f7..80a9568 100644 --- a/converter/converter.go +++ b/converter/converter.go @@ -36,7 +36,7 @@ func Convert(context *context.Context) error { if err != nil { return err } - format := chooseformat(rawTemplate) + format := detectFormatFromContent(rawTemplate) var outputTemplate []byte if format == "JSON" { @@ -113,7 +113,7 @@ func saveToFile(template []byte, path string, logger *logger.Logger) error { return nil } -func chooseformat(rawTemplate []byte) (format string) { +func detectFormatFromContent(rawTemplate []byte) (format string) { _, errorYAML := toYAML(rawTemplate) _, errorJSON := yamlToJSON(rawTemplate) @@ -123,5 +123,5 @@ func chooseformat(rawTemplate []byte) (format string) { } else if errorJSON == nil { return "YAML" } - return "Invalid output file format. Use JSON or YAML" + return "Unsupported file format. The input file must be either a valid _JSON_ or _YAML_ file." } From 037b3a1b11f5b765cc69852facb414fc5a450aa8 Mon Sep 17 00:00:00 2001 From: sgargula Date: Tue, 13 Feb 2018 12:46:44 +0100 Subject: [PATCH 3/9] format type file and fixFunctions --- .preprocessed.yml | 168 ++++++++++++++++++++++++++++++ converter/converter.go | 22 ++-- intrinsicsolver/.preprocessed.yml | 2 + 3 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 .preprocessed.yml create mode 100644 intrinsicsolver/.preprocessed.yml diff --git a/.preprocessed.yml b/.preprocessed.yml new file mode 100644 index 0000000..7e16f0b --- /dev/null +++ b/.preprocessed.yml @@ -0,0 +1,168 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: > + HB S2S Creatives Scripts stack which creates all necessary dependencies for that, based on environment. Including + external Elastic IP needed for production system, for communication with 3rd party used for creatives scanning done + on our side. + +Parameters: + + ENV: + Description: > + This points out which region and stage should be used - we call it an environment. + Type: String + Default: eu-central-1.dev + AllowedValues: + - eu-central-1.dev + - eu-central-1.qa + - eu-central-1.prod + - ap-southeast-1.prod + - us-east-1.prod + - us-west-2.prod + ConstraintDescription: > + Must be one of the defined allowed values. + +Mappings: + + EnvSettings: + eu-central-1.dev: + SecurityGroupId: sg-b30848da # private + SubnetIds: + - subnet-74c0a61d # eu-central-1a, private + - subnet-db7cf8a0 # eu-central-1b, private + HostedZone: dev.adtech.aolcloud.net + AmiId: ami-183a9e77 + Stage: dev + Capacity: 1 + OrbProjectId: "68748238" + + eu-central-1.qa: + SecurityGroupId: sg-4b074722 # private + SubnetIds: + - subnet-8bc2a4e2 # eu-central-1a, private + - subnet-0873f773 # eu-central-1b, private + HostedZone: qa.adtech.aolcloud.net + AmiId: ami-d8389cb7 + Stage: qa + Capacity: 1 + OrbProjectId: "68748252" + + eu-central-1.prod: + SecurityGroupId: sg-5af3b233 # public + SubnetIds: + - subnet-e06ce89b # eu-central-1b, public + - subnet-10f39579 # eu-central-1a, public + HostedZone: prod.adtech.aolcloud.net + AmiId: ami-673f9208 + Stage: prod + Capacity: 2 + OrbProjectId: "68748228" + + ap-southeast-1.prod: + SecurityGroupId: sg-672bc601 # public + SubnetIds: + - subnet-91a8a2e7 # ap-southeast-1a, public + - subnet-54907133 # ap-southeast-1b, public + HostedZone: prod.adtech.aolcloud.net + AmiId: ami-fd196f9e + Stage: prod + Capacity: 2 + OrbProjectId: "68748228" + + us-east-1.prod: + SecurityGroupId: sg-d4cbd4af # public + SubnetIds: + - subnet-56b18e0e # us-east-1a, public + - subnet-3d291617 # us-east-1c, public + HostedZone: prod.adtech.aolcloud.net + AmiId: ami-c81935de + Stage: prod + Capacity: 2 + OrbProjectId: "68748228" + + us-west-2.prod: + SecurityGroupId: sg-1f2e7f79 # public + SubnetIds: + - subnet-adc286c9 # us-west-2a, public + - subnet-fa30ac8c # us-west-2b, public + HostedZone: prod.adtech.aolcloud.net + AmiId: ami-7b8c8602 + Stage: prod + Capacity: 2 + OrbProjectId: "68748228" + +Conditions: + OnlyProduction: { "Fn::Equals": [ prod, "Fn::FindInMap": [ EnvSettings, "Ref": ENV, Stage ] ]} + +Resources: + + # We need 2 ElasticIPs and it will require manual + # association after creating it. No scripting for it. + + YumRepositoryAccessRole: + Type: AWS::IAM::Role + Properties: + RoleName: + Fn::Sub: + - "adtech-s2s-yum-access-rtb-cs-${AWS::Region}-${Stage}" + - Stage: { "Fn::FindInMap": [ EnvSettings, "Ref": ENV, Stage ]} + Policies: + - PolicyName: + Fn::Sub: + - "adtech-s2s-yum-access-policy-rtb-cs-${AWS::Region}-${Stage}" + - Stage: { "Fn::FindInMap": [ EnvSettings, "Ref": ENV, Stage ]} + PolicyDocument: + Version: "2012-10-17" + AvailabilityZone: + Fn::Select: + - "Fn::FindInMap": [ RegionMap, "Ref": "AWS::Region", 32 ] + - Fn::GetAZs: + Ref: "AWS::Region" + Statement: + - Action: + - s3:ListAllMyBuckets + Effect: Allow + Resource: + - arn:aws:s3:::* + - Action: + - s3:ListBucket + - s3:GetBucketLocation + Effect: Allow + Resource: + - Fn::Sub: + - "arn:aws:s3:::${BucketName}" + - BucketName: { "Fn::ImportValue": "adtech-s2s-rtb-yum-repository-bucket-name"} + - Action: + - s3:* + Effect: Allow + Resource: + - Fn::Sub: + - "arn:aws:s3:::${BucketName}/*" + - BucketName: { "Fn::ImportValue": "adtech-s2s-rtb-yum-repository-bucket-name"} + + CreativesScriptsLaunchConfiguration: + Type: AWS::AutoScaling::LaunchConfiguration + DependsOn: + - ReadAccessToYumRepository + Properties: + InstanceType: m4.xlarge + InstanceMonitoring: true + IamInstanceProfile: { "Fn::GetAtt": "ReadAccessToYumRepository.Arn"} + ImageId: { "Fn::FindInMap": [ EnvSettings, "Ref": ENV, AmiId ]} + SecurityGroups: + - "Fn::FindInMap": [ EnvSettings, "Ref": ENV, SecurityGroupId ] + EbsOptimized: true + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 30 + UserData: + "Fn::Base64": + "Fn::Sub": + #!/bin/bash -xe + /usr/bin/cfn-init -v --stack ${AWS::StackName} --resource CreativesScriptsLaunchConfiguration --region ${AWS::Region} + /usr/bin/cfn-signal --exit-code $? --stack ${AWS::StackName} --resource CreativesScriptsAutoScalingGroup --region ${AWS::Region} + + +Outputs: {} + diff --git a/converter/converter.go b/converter/converter.go index 5e8ef41..8e15345 100644 --- a/converter/converter.go +++ b/converter/converter.go @@ -21,14 +21,13 @@ package converter import ( "encoding/json" "errors" - "io/ioutil" - "os" - "github.com/Appliscale/perun/context" "github.com/Appliscale/perun/intrinsicsolver" "github.com/Appliscale/perun/logger" "github.com/asaskevich/govalidator" "github.com/ghodss/yaml" + "io/ioutil" + "os" ) // Read template from the file, convert it and check if it has valid structure. @@ -41,16 +40,16 @@ func Convert(context *context.Context) error { format := detectFormatFromContent(rawTemplate) var outputTemplate []byte + // If input type file is JSON convert to YAML. if format == "JSON" { outputTemplate, err = toYAML(rawTemplate) if err != nil { return err } saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) + + // If input type file is YAML, check all functions and create JSON (with or not --pretty-print flag). } else if format == "YAML" { - if !govalidator.IsJSON(string(rawTemplate)) { - return errors.New("This is not a valid YAML file") - } preprocessed, preprocessingError := intrinsicsolver.FixFunctions(rawTemplate, context.Logger, "multiline", "elongate", "correctlong") if preprocessingError != nil { context.Logger.Error(preprocessingError.Error()) @@ -63,14 +62,14 @@ func Convert(context *context.Context) error { if err != nil { return err } + err = saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) + if err != nil { + return err + } } else { context.Logger.Always(format) return nil } - err = saveToFile(outputTemplate, *context.CliArguments.OutputFilePath, context.Logger) - if err != nil { - return err - } return nil } @@ -96,9 +95,6 @@ func yamlToPrettyJSON(yamlTemplate []byte) ([]byte, error) { jsonTemplate, templateError := json.MarshalIndent(YAMLObj, "", " ") - if !govalidator.IsJSON(string(jsonTemplate)) { - return nil, errors.New("This is not a valid YAML file") - } return jsonTemplate, templateError } diff --git a/intrinsicsolver/.preprocessed.yml b/intrinsicsolver/.preprocessed.yml new file mode 100644 index 0000000..32a417d --- /dev/null +++ b/intrinsicsolver/.preprocessed.yml @@ -0,0 +1,2 @@ +Key: { "Fn::Equals": [ value_1, "Fn::FindInMap": [ MapName, "Ref": TopLevelKeyRef, SecondLevelKey ] ]} + From b7644180327c58385a8d3299840c3f14936029af Mon Sep 17 00:00:00 2001 From: sgargula Date: Wed, 21 Mar 2018 13:47:07 +0100 Subject: [PATCH 4/9] debug --- .preprocessed.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .preprocessed.yml diff --git a/.preprocessed.yml b/.preprocessed.yml deleted file mode 100644 index 56f1a3b..0000000 --- a/.preprocessed.yml +++ /dev/null @@ -1,6 +0,0 @@ -Resources: - HelloBucket: - Properties: - AccessControl: - Type: AWS::S3::Bucket - From 69bc5d94ea46e89f36172b7d4e24e7a59930ecd1 Mon Sep 17 00:00:00 2001 From: sgargula Date: Wed, 21 Mar 2018 13:52:29 +0100 Subject: [PATCH 5/9] remove file --- intrinsicsolver/.preprocessed.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 intrinsicsolver/.preprocessed.yml diff --git a/intrinsicsolver/.preprocessed.yml b/intrinsicsolver/.preprocessed.yml deleted file mode 100644 index 32a417d..0000000 --- a/intrinsicsolver/.preprocessed.yml +++ /dev/null @@ -1,2 +0,0 @@ -Key: { "Fn::Equals": [ value_1, "Fn::FindInMap": [ MapName, "Ref": TopLevelKeyRef, SecondLevelKey ] ]} - From 04c871ffd4686a608944c143abf2dbbb1ab572cb Mon Sep 17 00:00:00 2001 From: piwowarc Date: Wed, 4 Apr 2018 12:08:45 +0200 Subject: [PATCH 6/9] fix logging --- cliparser/cliparser.go | 10 +++++++--- stack/stack.go | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cliparser/cliparser.go b/cliparser/cliparser.go index 67d4499..6a2ac54 100644 --- a/cliparser/cliparser.go +++ b/cliparser/cliparser.go @@ -46,6 +46,7 @@ type CliArguments struct { Region *string Sandbox *bool Stack *string + Capabilities *[]string PrettyPrint *bool } @@ -77,13 +78,15 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { configure = app.Command(ConfigureMode, "Create your own configuration mode") - createStack = app.Command(CreateStackMode, "Creates a stack on aws") - createStackName = createStack.Arg("stack", "An AWS stack name.").Required().String() - createStackTemplate = createStack.Arg("template", "A path to the template file.").Required().String() + createStack = app.Command(CreateStackMode, "Creates a stack on aws") + createStackName = createStack.Arg("stack", "An AWS stack name.").Required().String() + createStackTemplate = createStack.Arg("template", "A path to the template file.").Required().String() + createStackCapabilities = createStack.Flag("capabilities", "Capabilities: CAPABILITY_IAM | CAPABILITY_NAMED_IAM").Enums("CAPABILITY_IAM", "CAPABILITY_NAMED_IAM") deleteStack = app.Command(DestroyStackMode, "Deletes a stack on aws") deleteStackName = deleteStack.Arg("stack", "An AWS stack name.").Required().String() ) + app.HelpFlag.Short('h') app.Version(utilities.VersionStatus()) @@ -115,6 +118,7 @@ func ParseCliArguments(args []string) (cliArguments CliArguments, err error) { cliArguments.Mode = &CreateStackMode cliArguments.TemplatePath = createStackTemplate cliArguments.Stack = createStackName + cliArguments.Capabilities = createStackCapabilities // delete Stack case deleteStack.FullCommand(): diff --git a/stack/stack.go b/stack/stack.go index 0f49f2c..f395e41 100644 --- a/stack/stack.go +++ b/stack/stack.go @@ -10,9 +10,17 @@ import ( // This function gets template and name of stack. It creates "CreateStackInput" structure. func createStackInput(context *context.Context, template *string, stackName *string) cloudformation.CreateStackInput { + + rawCapabilities := *context.CliArguments.Capabilities + capabilities := make([]*string, len(rawCapabilities)) + for i, capability := range rawCapabilities { + capabilities[i] = &capability + } + templateStruct := cloudformation.CreateStackInput{ TemplateBody: template, StackName: stackName, + Capabilities: capabilities, } return templateStruct } @@ -32,9 +40,10 @@ func getTemplateFromFile(context *context.Context) (string, string) { } // This function uses CreateStackInput variable to create Stack. -func createStack(templateStruct cloudformation.CreateStackInput, session *session.Session) { +func createStack(templateStruct cloudformation.CreateStackInput, session *session.Session) (err error) { api := cloudformation.New(session) - api.CreateStack(&templateStruct) + _, err = api.CreateStack(&templateStruct) + return } // This function uses all functions above and session to create Stack. @@ -49,7 +58,10 @@ func NewStack(context *context.Context) { if createSessionError != nil { context.Logger.Error(createSessionError.Error()) } - createStack(templateStruct, session) + createStackError := createStack(templateStruct, session) + if createStackError != nil { + context.Logger.Error(createStackError.Error()) + } } // This function bases on "DeleteStackInput" structure and destroys stack. It uses "StackName" to choose which stack will be destroy. Before that it creates session. From 84b4988e31f9e3c042c20ffb692a709b5039cfea Mon Sep 17 00:00:00 2001 From: piwowarc Date: Wed, 4 Apr 2018 14:07:50 +0200 Subject: [PATCH 7/9] update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c51495..943f4f7 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Then type path and name of new configuration file. To create new stack you have to type: -``~ $ perun create-stack ] `` + ### Configuration file You can find an example configuration file in the main directory of the repository in file `defaults/main.yml`. @@ -139,6 +140,12 @@ Example JSON template which describe S3 Bucket: If you want to destroy stack just type its name. Before you create stack you should validate it with perun :wink:. +### Capabilities +If your template includes resources that can affect permissions in your AWS account, +you must explicitly acknowledge its capabilities by adding `--capabilities=CAPABILITIES` flag. + +Valid values are `CAPABILITY_IAM` and `CAPABILITY_NAMED_IAM`. + ## License [Apache License 2.0](LICENSE) From b7204123e93380e3a5d7bde47b0170b915179d55 Mon Sep 17 00:00:00 2001 From: piwowarc Date: Wed, 4 Apr 2018 15:59:14 +0200 Subject: [PATCH 8/9] update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 943f4f7..c05a96b 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,12 @@ If you want to destroy stack just type its name. Before you create stack you should validate it with perun :wink:. ### Capabilities + If your template includes resources that can affect permissions in your AWS account, -you must explicitly acknowledge its capabilities by adding `--capabilities=CAPABILITIES` flag. +you must explicitly acknowledge its capabilities by adding `--capabilities=CAPABILITY` flag. Valid values are `CAPABILITY_IAM` and `CAPABILITY_NAMED_IAM`. +You can specify both of them by adding `--capabilities=CAPABILITY_IAM --capabilities=CAPABILITY_NAMED_IAM`. ## License From 877c047ec2dc34ca8615aa303dd6055f12d50010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojtek=20Gawro=C5=84ski?= Date: Fri, 20 Apr 2018 10:14:24 +0200 Subject: [PATCH 9/9] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c05a96b..216a2c7 100644 --- a/README.md +++ b/README.md @@ -157,12 +157,14 @@ You can specify both of them by adding `--capabilities=CAPABILITY_IAM --capabili - [Piotr Figwer](https://github.com/pfigwer) - [Sylwia Gargula](https://github.com/SylwiaGargula) - [Wojciech Gawroński](https://github.com/afronski) -- [Jakub Lamparski](https://github.com/jlampar) +- [Mateusz Piwowarczyk](https://github.com/piwowarc) ## Contributors +- [Jakub Lamparski](https://github.com/jlampar) - [Aleksander Mamla](https://github.com/amamla) - [Kacper Patro](https://github.com/morfeush22) - [Paweł Pikuła](https://github.com/ppikula) - [Michał Połcik](https://github.com/mwpolcik) +- [Tomasz Raus](https://github.com/rusty-2) - [Maksymilian Wojczuk](https://github.com/maxiwoj)