Skip to content

Commit

Permalink
Merge branch 'main' into fix-saas-clusterid-ref
Browse files Browse the repository at this point in the history
  • Loading branch information
johnBgood authored Jan 20, 2025
2 parents dbf75e8 + 4231107 commit 8d842b9
Show file tree
Hide file tree
Showing 18 changed files with 341 additions and 36 deletions.
95 changes: 95 additions & 0 deletions .github/workflows/ENFORCE_QA_APPROVAL.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Enforce Specific QA Approval for "feat" PRs

on:
pull_request:
types: [opened, edited, synchronize]
pull_request_review:
types: [ submitted ]

jobs:
enforce-approval:
runs-on: ubuntu-latest

steps:
- name: Import Secrets
id: vault-secrets
uses: hashicorp/[email protected]
with:
url: ${{ secrets.VAULT_ADDR }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID}}
secrets: |
secret/data/products/connectors/ci/common GITHUB_APP_ID;
secret/data/products/connectors/ci/common GITHUB_APP_PRIVATE_KEY;
- name: Generate a GitHub token for connectors
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ steps.vault-secrets.outputs.GITHUB_APP_ID }}
private-key: ${{ steps.vault-secrets.outputs.GITHUB_APP_PRIVATE_KEY }}
- name: Check PR Title and Specific Dev Approval
uses: actions/github-script@v6
with:
script: |
const prTitle = context.payload.pull_request.title;
const prNumber = context.payload.pull_request.number;
const repoOwner = context.repo.owner;
const repoName = context.repo.repo;
const octokit = new github.constructor({
auth: process.env.TOKEN,
});
const teamMembersResponse = await octokit.rest.teams.listMembersInOrg({
org: "camunda",
team_slug: "qa-engineering",
})
const teamMembers = teamMembersResponse.data.map(tm => tm.login);
console.log("team members are :" + JSON.stringify(teamMembers));
const reviews = await github.rest.pulls.listReviews({
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
});
const hasApproved = reviews.data.some(review =>
teamMembers.includes(review.user.login) && review.state === "APPROVED"
);
if (prTitle.startsWith("feat")) {
if (hasApproved) {
await github.rest.repos.createCommitStatus({
owner: repoOwner,
repo: repoName,
sha: context.payload.pull_request.head.sha,
state: "success",
context: "QA-approval",
description: `a QA has approved this PR.`,
});
} else {
await github.rest.repos.createCommitStatus({
owner: repoOwner,
repo: repoName,
sha: context.payload.pull_request.head.sha,
state: "failure",
context: "QA-approval",
description: `a QA approval is required for 'feat' PRs.`,
});
}
} else {
await github.rest.repos.createCommitStatus({
owner: repoOwner,
repo: repoName,
sha: context.payload.pull_request.head.sha,
state: "success",
context: "QA-approval",
description: "No specific approval required for non-'feat' PRs.",
});
}
env:
TOKEN: ${{ steps.app-token.outputs.token }}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class ConnectorsObjectMapperSupplier {
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.build();

private ConnectorsObjectMapperSupplier() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,53 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel;
import io.camunda.connector.document.annotation.jackson.JacksonModuleDocumentDeserializer.DocumentModuleSettings;
import io.camunda.document.Document;
import io.camunda.document.factory.DocumentFactoryImpl;
import io.camunda.document.store.InMemoryDocumentStore;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

class ConnectorsObjectMapperSupplierTest {

@Test
void objectMapperConfigTest() throws JsonProcessingException {
void java8DatesShouldBeSupported() throws JsonProcessingException {
final var objectMapper = ConnectorsObjectMapperSupplier.getCopy();
final var json = "{\"data\":\"2024-01-01\"}";
final var jsonDeserialized = Map.of("data", LocalDate.of(2024, 1, 1));
Assertions.assertThat(objectMapper.writeValueAsString(jsonDeserialized)).isEqualTo(json);
var actual = objectMapper.readValue(json, new TypeReference<Map<String, LocalDate>>() {});
Assertions.assertThat(actual).isEqualTo(jsonDeserialized);
}

@Test
void singlePrimitiveValueShouldBeAcceptedAsArray() throws JsonProcessingException {
final var objectMapper = ConnectorsObjectMapperSupplier.getCopy();
final var json = "1";
var actual = objectMapper.readValue(json, int[].class);
Assertions.assertThat(actual).isEqualTo(new int[] {1});
}

@Test
void singleDocumentShouldBeAcceptedAsArray() throws JsonProcessingException {
final var objectMapper =
ConnectorsObjectMapperSupplier.getCopy(
new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE),
DocumentModuleSettings.create());
final var documentReference =
new CamundaDocumentReferenceModel(
"default", UUID.randomUUID().toString(), null, Optional.empty());
final var json = "{\"documents\":" + objectMapper.writeValueAsString(documentReference) + "}";
var actual = objectMapper.readValue(json, TestRecordWithDocumentList.class);
Assertions.assertThat(actual.documents()).hasSize(1);
Assertions.assertThat(actual.documents().get(0).reference()).isEqualTo(documentReference);
}

private record TestRecordWithDocumentList(List<Document> documents) {}
}
15 changes: 14 additions & 1 deletion connectors/gitlab/element-templates/gitlab-connector.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json",
"name": "GitLab Outbound Connector",
"id": "io.camunda.connectors.GitLab.v1",
"version": 5,
"version": 6,
"description": "Manage GitLab issues, branches, releases, and more",
"metadata": {
"keywords": [
Expand Down Expand Up @@ -654,6 +654,19 @@
]
}
},
{
"id": "skipEncoding",
"label": "Skip URL encoding",
"description": "Skip the default URL decoding and encoding behavior",
"optional": true,
"group": "endpoint",
"binding": {
"name": "skipEncoding",
"type": "zeebe:input"
},
"value": "true",
"type": "Hidden"
},
{
"label": "Issue ID",
"description": "The internal ID of a project’s issue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ public HttpCommonResult execute(
.execute(
apacheRequest,
new HttpCommonResultResponseHandler(
executionEnvironment, request.isStoreResponse()));
executionEnvironment,
request.isStoreResponse(),
request.getGroupSetCookieHeaders()));
if (HttpStatusHelper.isError(result.status())) {
throw ConnectorExceptionMapper.from(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -51,23 +49,27 @@ public class HttpCommonResultResponseHandler

private final boolean isStoreResponseSelected;

private final boolean groupSetCookieHeaders;

public HttpCommonResultResponseHandler(
@Nullable ExecutionEnvironment executionEnvironment, boolean isStoreResponseSelected) {
@Nullable ExecutionEnvironment executionEnvironment,
boolean isStoreResponseSelected,
boolean groupSetCookieHeaders) {
this.executionEnvironment = executionEnvironment;
this.isStoreResponseSelected = isStoreResponseSelected;
this.fileResponseHandler =
new FileResponseHandler(executionEnvironment, isStoreResponseSelected);
this.groupSetCookieHeaders = groupSetCookieHeaders;
}

@Override
public HttpCommonResult handleResponse(ClassicHttpResponse response) {
int code = response.getCode();
String reason = response.getReasonPhrase();
Map<String, String> headers =
Arrays.stream(response.getHeaders())
.collect(
// Collect the headers into a map ignoring duplicates (Set Cookies for instance)
Collectors.toMap(Header::getName, Header::getValue, (first, second) -> first));
Map<String, Object> headers =
HttpCommonResultResponseHandler.formatHeaders(
response.getHeaders(), this.groupSetCookieHeaders);

if (response.getEntity() != null) {
try (InputStream content = response.getEntity().getContent()) {
if (executionEnvironment instanceof ExecutionEnvironment.SaaSCluster) {
Expand All @@ -88,7 +90,30 @@ public HttpCommonResult handleResponse(ClassicHttpResponse response) {
return new HttpCommonResult(code, headers, null, reason);
}

private Document handleFileResponse(Map<String, String> headers, byte[] content) {
private static Map<String, Object> formatHeaders(
Header[] headersArray, Boolean groupSetCookieHeaders) {
return Arrays.stream(headersArray)
.collect(
Collectors.toMap(
Header::getName,
header -> {
if (groupSetCookieHeaders && header.getName().equalsIgnoreCase("Set-Cookie")) {
return new ArrayList<String>(List.of(header.getValue()));
} else {
return header.getValue();
}
},
(existingValue, newValue) -> {
if (groupSetCookieHeaders
&& existingValue instanceof List
&& newValue instanceof List) {
((List<String>) existingValue).add(((List<String>) newValue).getFirst());
}
return existingValue;
}));
}

private Document handleFileResponse(Map<String, Object> headers, byte[] content) {
var document = fileResponseHandler.handle(headers, content);
LOGGER.debug("Stored response as document. Document reference: {}", document);
return document;
Expand All @@ -99,7 +124,7 @@ private Document handleFileResponse(Map<String, String> headers, byte[] content)
* unwrapped as an ErrorResponse. Otherwise, it will be unwrapped as a HttpCommonResult.
*/
private HttpCommonResult getResultForCloudFunction(
int code, InputStream content, Map<String, String> headers, String reason)
int code, InputStream content, Map<String, Object> headers, String reason)
throws IOException {
if (HttpStatusHelper.isError(code)) {
// unwrap as ErrorResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public class ApacheRequestUriBuilder implements ApacheRequestPartBuilder {

@Override
public void build(ClassicRequestBuilder builder, HttpCommonRequest request) {
builder.setUri(UrlEncoder.toEncodedUri(request.getUrl()));
builder.setUri(UrlEncoder.toEncodedUri(request.getUrl(), request.getSkipEncoding()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@
public class UrlEncoder {
private static final Logger LOG = LoggerFactory.getLogger(ApacheRequestUriBuilder.class);

public static URI toEncodedUri(String requestUrl) {
public static URI toEncodedUri(String requestUrl, Boolean skipEncoding) {
try {
// We try to decode the URL first, because it might be encoded already
// which would lead to double encoding. Decoding is safe here, because it does nothing if
// the URL is not encoded.
if (skipEncoding) {
return URI.create(requestUrl);
}
var decodedUrl = URLDecoder.decode(requestUrl, StandardCharsets.UTF_8);
var url = new URL(decodedUrl);
// Only this URI constructor escapes the URL properly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public Document handleCloudFunctionResult(HttpCommonResult result) {
return null;
}

public Document handle(Map<String, String> headers, byte[] content) {
public Document handle(Map<String, Object> headers, byte[] content) {
if (storeResponseSelected()
&& executionEnvironment instanceof ExecutionEnvironment.StoresDocument env) {
try (var byteArrayInputStream = new ByteArrayInputStream(content)) {
Expand All @@ -72,10 +72,11 @@ public Document handle(Map<String, String> headers, byte[] content) {
return null;
}

private String getContentType(Map<String, String> headers) {
private String getContentType(Map<String, Object> headers) {
return headers.entrySet().stream()
.filter(e -> e.getKey().equalsIgnoreCase(CONTENT_TYPE))
.map(Map.Entry::getValue)
.map(Object::toString)
.findFirst()
.orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class ConnectorExceptionMapper {
public static ConnectorException from(HttpCommonResult result) {
String status = String.valueOf(result.status());
String reason = Optional.ofNullable(result.reason()).orElse("[no reason]");
Map<String, String> headers = result.headers();
Map<String, Object> headers = result.headers();
Object body = result.body();
Map<String, Object> response = new HashMap<>();
response.put("headers", headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ public class HttpCommonRequest {
description = "Store the response as a document in the document store")
private boolean storeResponse;

@TemplateProperty(
label = "Skip URL encoding",
description = "Skip the default URL decoding and encoding behavior",
type = TemplateProperty.PropertyType.Hidden,
feel = Property.FeelMode.disabled,
group = "endpoint",
optional = true)
private String skipEncoding;

@TemplateProperty(
label = "Group set-cookie headers to a list",
description =
"Group incoming headers with same name into a List to support <a href=\"https://datatracker.ietf.org/doc/html/rfc6265\">multiple Set-Cookie headers</a>.",
type = TemplateProperty.PropertyType.Hidden,
feel = Property.FeelMode.disabled,
group = "endpoint",
optional = true)
private String groupSetCookieHeaders;

public Object getBody() {
return body;
}
Expand Down Expand Up @@ -139,6 +158,22 @@ public void setQueryParameters(Map<String, String> queryParameters) {
this.queryParameters = queryParameters;
}

public boolean getSkipEncoding() {
return Objects.equals(skipEncoding, "true");
}

public void setSkipEncoding(final String skipEncoding) {
this.skipEncoding = skipEncoding;
}

public boolean getGroupSetCookieHeaders() {
return Objects.equals(groupSetCookieHeaders, "true");
}

public void setGroupSetCookieHeaders(final String groupSetCookieHeaders) {
this.groupSetCookieHeaders = groupSetCookieHeaders;
}

public boolean hasAuthentication() {
return authentication != null;
}
Expand Down
Loading

0 comments on commit 8d842b9

Please sign in to comment.