Skip to content

Commit

Permalink
feat(codebuild): Add support for triggering and monitoring AWS CodeBu… (
Browse files Browse the repository at this point in the history
#595)

* feat(codebuild): Add support for triggering and monitoring AWS CodeBuild builds

* Use STS AssumeRole instead of static credentials
  • Loading branch information
Kaixiang-AWS authored Feb 4, 2020
1 parent 025ff99 commit 8422a0a
Show file tree
Hide file tree
Showing 12 changed files with 575 additions and 0 deletions.
1 change: 1 addition & 0 deletions igor-web/igor-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
implementation "org.springframework.security:spring-security-config"
implementation "org.springframework.security:spring-security-web"

implementation "com.amazonaws:aws-java-sdk"
implementation "com.google.apis:google-api-services-cloudbuild"
implementation "com.google.apis:google-api-services-storage"
implementation 'com.google.auth:google-auth-library-oauth2-http'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.codebuild;

import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider;
import com.amazonaws.services.codebuild.AWSCodeBuildClient;
import com.amazonaws.services.codebuild.AWSCodeBuildClientBuilder;
import com.amazonaws.services.codebuild.model.BatchGetBuildsRequest;
import com.amazonaws.services.codebuild.model.Build;
import com.amazonaws.services.codebuild.model.StartBuildRequest;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;

/** Generates authenticated requests to AWS CodeBuild API for a single configured account */
@RequiredArgsConstructor
public class AwsCodeBuildAccount {
private final AWSCodeBuildClient client;

@Autowired AWSSecurityTokenServiceClient stsClient;

public AwsCodeBuildAccount(String accountId, String assumeRole, String region) {
STSAssumeRoleSessionCredentialsProvider credentialsProvider =
new STSAssumeRoleSessionCredentialsProvider.Builder(
getRoleArn(accountId, assumeRole), "spinnaker-session")
.withStsClient(stsClient)
.build();

// TODO: Add client-side rate limiting to avoid getting throttled if necessary
this.client =
(AWSCodeBuildClient)
AWSCodeBuildClientBuilder.standard()
.withCredentials(credentialsProvider)
.withRequestHandlers(new AwsCodeBuildRequestHandler())
.withRegion(region)
.build();
}

public Build startBuild(StartBuildRequest request) {
return client.startBuild(request).getBuild();
}

public Build getBuild(String buildId) {
return client.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds().get(0);
}

private String getRoleArn(String accountId, String assumeRole) {
String assumeRoleValue = Objects.requireNonNull(assumeRole, "assumeRole");
if (!assumeRoleValue.startsWith("arn:")) {
/**
* GovCloud and China regions need to have the full arn passed because of differing formats
* Govcloud: arn:aws-us-gov:iam China: arn:aws-cn:iam
*/
assumeRoleValue =
String.format(
"arn:aws:iam::%s:%s",
Objects.requireNonNull(accountId, "accountId"), assumeRoleValue);
}
return assumeRoleValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.codebuild;

import com.netflix.spinnaker.kork.web.exceptions.NotFoundException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AwsCodeBuildAccountRepository {
private final Map<String, AwsCodeBuildAccount> accounts = new HashMap<>();

public void addAccount(String name, AwsCodeBuildAccount service) {
accounts.put(name, service);
}

public AwsCodeBuildAccount getAccount(String name) {
AwsCodeBuildAccount account = accounts.get(name);
if (account == null) {
throw new NotFoundException(
String.format("No AWS CodeBuild account with name %s is configured", name));
}
return account;
}

public List<String> getAccountNames() {
return accounts.keySet().stream().sorted().collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.codebuild;

import com.amazonaws.services.codebuild.model.Build;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@ConditionalOnProperty("codebuild.enabled")
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/codebuild")
public class AwsCodeBuildController {
private final AwsCodeBuildAccountRepository awsCodeBuildAccountRepository;

@RequestMapping(value = "/accounts", method = RequestMethod.GET)
List<String> getAccounts() {
return awsCodeBuildAccountRepository.getAccountNames();
}

@RequestMapping(
value = "/builds/start/{account}",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE)
Build startBuild(@PathVariable String account, @RequestBody Map<String, Object> requestBody) {
return awsCodeBuildAccountRepository
.getAccount(account)
.startBuild(AwsCodeBuildMapper.toStartBuildRequest(requestBody));
}

@RequestMapping(value = "/builds/{account}/{buildId}", method = RequestMethod.GET)
Build getBuild(@PathVariable String account, @PathVariable String buildId) {
return awsCodeBuildAccountRepository.getAccount(account).getBuild(buildId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.codebuild;

import com.amazonaws.services.codebuild.model.StartBuildRequest;
import java.util.Map;

public class AwsCodeBuildMapper {
private static final String PROJECT_NAME = "projectName";

public static StartBuildRequest toStartBuildRequest(Map<String, Object> requestBody) {
String projectName = (String) requestBody.get(PROJECT_NAME);
return new StartBuildRequest().withProjectName(projectName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.netflix.spinnaker.igor.codebuild;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.handlers.RequestHandler2;
import com.netflix.spinnaker.igor.exceptions.BuildJobError;
import com.netflix.spinnaker.security.AuthenticatedRequest;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AwsCodeBuildRequestHandler extends RequestHandler2 {
@Override
public AmazonWebServiceRequest beforeMarshalling(AmazonWebServiceRequest request) {
final String userAgent =
String.format(
"spinnaker-user/%s spinnaker-executionId/%s",
AuthenticatedRequest.getSpinnakerUser().orElse("unknown"),
AuthenticatedRequest.getSpinnakerExecutionId().orElse("unknown"));

final AmazonWebServiceRequest cloned = request.clone();

cloned.getRequestClientOptions().appendUserAgent(userAgent);
return super.beforeMarshalling(cloned);
}

@Override
public void afterError(Request<?> request, Response<?> response, Exception e) {
if (e instanceof AmazonServiceException
&& ((AmazonServiceException) e)
.getErrorType()
.equals(AmazonServiceException.ErrorType.Client)) {
log.warn(e.getMessage());
throw new BuildJobError(e.getMessage());
} else {
log.error(e.getMessage());
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.config;

import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import com.netflix.spinnaker.igor.codebuild.AwsCodeBuildAccount;
import com.netflix.spinnaker.igor.codebuild.AwsCodeBuildAccountRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnProperty("codebuild.enabled")
@EnableConfigurationProperties({AwsCodeBuildProperties.class})
public class AwsCodeBuildConfig {
@Bean
AwsCodeBuildAccountRepository awsCodeBuildAccountRepository(
AwsCodeBuildProperties awsCodeBuildProperties) {
AwsCodeBuildAccountRepository accounts = new AwsCodeBuildAccountRepository();
awsCodeBuildProperties
.getAccounts()
.forEach(
a -> {
AwsCodeBuildAccount account =
new AwsCodeBuildAccount(a.getAccountId(), a.getAssumeRole(), a.getRegion());
accounts.addAccount(a.getName(), account);
});
return accounts;
}

@Bean
AWSSecurityTokenServiceClient awsSecurityTokenServiceClient() {
return (AWSSecurityTokenServiceClient)
AWSSecurityTokenServiceClientBuilder.standard()
.withCredentials(DefaultAWSCredentialsProviderChain.getInstance())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2020 Amazon.com, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.igor.config;

import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "codebuild")
public class AwsCodeBuildProperties {
private List<Account> accounts;

@Data
public static class Account {
private String name;
private String region;
private String accountId;
private String assumeRole;
}
}
Loading

0 comments on commit 8422a0a

Please sign in to comment.