Skip to content

Commit ee77dce

Browse files
authored
Implement http step runtime using JDK http client. (#182)
1 parent 026fadb commit ee77dce

File tree

25 files changed

+1017
-13
lines changed

25 files changed

+1017
-13
lines changed

maestro-common/src/main/java/com/netflix/maestro/models/artifact/Artifact.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
@JsonSubTypes.Type(name = "NOTEBOOK", value = NotebookArtifact.class),
3333
@JsonSubTypes.Type(name = "DYNAMIC_OUTPUT", value = DynamicOutputArtifact.class),
3434
@JsonSubTypes.Type(name = "KUBERNETES", value = KubernetesArtifact.class),
35+
@JsonSubTypes.Type(name = "HTTP", value = HttpArtifact.class),
3536
})
3637
@SuppressWarnings("PMD.ImplicitFunctionalInterface")
3738
public interface Artifact {
@@ -55,7 +56,9 @@ enum Type {
5556
/** dynamic output (e.g. output signal) artifact. */
5657
DYNAMIC_OUTPUT(Constants.MAESTRO_PREFIX + "dynamic_output"),
5758
/** kubernetes artifact. */
58-
KUBERNETES(Constants.MAESTRO_PREFIX + "kubernetes");
59+
KUBERNETES(Constants.MAESTRO_PREFIX + "kubernetes"),
60+
/** http artifact. */
61+
HTTP(Constants.MAESTRO_PREFIX + "http");
5962

6063
private final String key;
6164

@@ -140,4 +143,13 @@ default DynamicOutputArtifact asDynamicOutput() {
140143
default KubernetesArtifact asKubernetes() {
141144
throw new MaestroInternalError("Artifact type [%s] cannot be used as KUBERNETES", getType());
142145
}
146+
147+
/**
148+
* Get http type artifact.
149+
*
150+
* @return concrete artifact object.
151+
*/
152+
default HttpArtifact asHttp() {
153+
throw new MaestroInternalError("Artifact type [%s] cannot be used as HTTP", getType());
154+
}
143155
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.netflix.maestro.models.artifact;
14+
15+
import com.fasterxml.jackson.annotation.JsonIgnore;
16+
import com.fasterxml.jackson.annotation.JsonInclude;
17+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
18+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
19+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
20+
import com.netflix.maestro.models.stepruntime.HttpStepRequest;
21+
import lombok.Data;
22+
import lombok.ToString;
23+
24+
/** HTTP artifact to store HTTP request and response info. */
25+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
26+
@JsonInclude(JsonInclude.Include.NON_NULL)
27+
@JsonPropertyOrder(
28+
value = {"request", "status", "status_code", "body"},
29+
alphabetic = true)
30+
@Data
31+
@ToString
32+
public class HttpArtifact implements Artifact {
33+
private HttpStepRequest request;
34+
private String status;
35+
private int statusCode;
36+
private String body;
37+
38+
@JsonIgnore
39+
@Override
40+
public HttpArtifact asHttp() {
41+
return this;
42+
}
43+
44+
@Override
45+
public Type getType() {
46+
return Type.HTTP;
47+
}
48+
}

maestro-common/src/main/java/com/netflix/maestro/models/definition/StepType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public enum StepType {
1818
NOTEBOOK("Notebook", true),
1919
/** Kubernetes step. */
2020
KUBERNETES("Kubernetes", true),
21+
/** HTTP/HTTPS step. */
22+
HTTP("Http", true),
2123
/** Join step. */
2224
JOIN("Join", false),
2325
/** foreach loop step. */
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.netflix.maestro.models.stepruntime;
14+
15+
import com.fasterxml.jackson.annotation.JsonInclude;
16+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
17+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
18+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
19+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
20+
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
21+
import java.util.Map;
22+
import lombok.Builder;
23+
import lombok.EqualsAndHashCode;
24+
import lombok.Getter;
25+
import lombok.ToString;
26+
27+
/** Http request data model for http step runtime. */
28+
@JsonDeserialize(builder = HttpStepRequest.HttpStepRequestBuilder.class)
29+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
30+
@JsonInclude(JsonInclude.Include.NON_NULL)
31+
@JsonPropertyOrder(
32+
value = {"url", "method", "headers", "body"},
33+
alphabetic = true)
34+
@Builder(toBuilder = true)
35+
@Getter
36+
@ToString
37+
@EqualsAndHashCode
38+
public class HttpStepRequest {
39+
private final String url;
40+
private final String method;
41+
private final Map<String, String> headers;
42+
private final String body;
43+
44+
/** builder class for lombok and jackson. */
45+
@JsonPOJOBuilder(withPrefix = "")
46+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
47+
public static final class HttpStepRequestBuilder {}
48+
}

maestro-engine/src/main/java/com/netflix/maestro/engine/params/OutputDataManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ private Optional<String> extractExternalJobId(StepRuntimeSummary runtimeSummary)
9898
jobId = artifacts.get(Artifact.Type.KUBERNETES.key()).asKubernetes().getJobId();
9999
} else if (artifacts.containsKey(Artifact.Type.TITUS.key())) {
100100
jobId = artifacts.get(Artifact.Type.TITUS.key()).asTitus().getTitusTaskId();
101+
} else if (artifacts.containsKey(Artifact.Type.HTTP.key())) {
102+
jobId = runtimeSummary.getStepInstanceUuid();
101103
}
102104
if (jobId != null && !jobId.isEmpty()) {
103105
return Optional.of(jobId);

maestro-engine/src/main/java/com/netflix/maestro/engine/transformation/StepTranslator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public TaskDef translate(Step step) {
3838
case TITUS:
3939
case NOTEBOOK:
4040
case KUBERNETES:
41+
case HTTP:
4142
return createMaestroTask((AbstractStep) step);
4243
default:
4344
throw new UnsupportedOperationException(

maestro-http/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Maestro HTTP Module
2+
3+
The Maestro Http module includes maestro http/https step runtime support to launch a job making an HTTP/HTTPS call.
4+
5+
## Overview
6+
7+
This module enables workflows to make HTTP/HTTPS calls as step executions and save the response in the http artifact and output params.
8+
9+
## Architecture
10+
11+
- **HttpRuntimeExecutor**: Interface for executing HTTP requests
12+
- **JdkHttpRuntimeExecutor**: Default implementation using JDK 11+ HttpClient
13+
- **HttpStepRuntime**: Step runtime that implements HTTP request execution and state management
14+
15+
## Step Parameters
16+
17+
HTTP steps accept the following parameters (see `default-http-step-params.yaml`):
18+
19+
- **http** (required): Map containing request configuration
20+
- **url** (required): URL for the HTTP request
21+
- **method** (provided): HTTP method (default: GET)
22+
- **headers** (provided): String Map of HTTP headers
23+
- **body** (optional): Request body in String format
24+
- **state_expr** (provided): SEL expression to determine step state based on response
25+
26+
## Output Parameters
27+
28+
HTTP steps produce the following output parameters:
29+
30+
- **status_code**: HTTP response status code (e.g., 200, 404, 500)
31+
- **response_body**: Response body as String format
32+
33+
## Usage Example
34+
35+
See `maestro-server/src/test/resources/samples/yaml/sample-http-wf.yaml` for a complete example.
36+
37+
## Security Considerations
38+
39+
**WARNING**: This module is currently in development and has known security limitations (e.g. SSRF).
40+
**Do not use in production environments accessible to untrusted users** until these issues are addressed.
41+
42+
## Pending Features
43+
44+
- Add URL validation for SSRF vulnerability
45+
- Add response size limits
46+
- Add response content-type handling
47+
- Add metrics

maestro-http/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
dependencies {
2+
implementation project(':maestro-common')
3+
implementation project(':maestro-engine')
4+
implementation jacksonDatabindDep
5+
api slf4jApiDep
6+
7+
testImplementation junitDep
8+
testImplementation mockitoCoreDep
9+
testImplementation(testFixtures(project(':maestro-common')))
10+
}

maestro-http/gradle.lockfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This is a Gradle generated file for dependency locking.
2+
# Manual edits can break the build and are not advised.
3+
# This file is expected to be part of source control.
4+
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath
5+
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath
6+
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath
7+
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath
8+
org.projectlombok:lombok:1.18.42=annotationProcessor,compileClasspath
9+
org.slf4j:slf4j-api:1.7.36=compileClasspath
10+
empty=
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.netflix.maestro.engine.http;
14+
15+
import com.netflix.maestro.models.stepruntime.HttpStepRequest;
16+
import java.io.IOException;
17+
import java.net.http.HttpResponse;
18+
19+
/** Interface for HTTP runtime executor to execute HTTP requests. */
20+
@FunctionalInterface
21+
public interface HttpRuntimeExecutor {
22+
/**
23+
* Execute an HTTP request.
24+
*
25+
* @param request the HTTP request to execute
26+
* @return the HTTP response
27+
* @throws IOException – if an I/O error occurs when sending or receiving, or the client has
28+
* closing shut down
29+
* @throws InterruptedException – if the operation is interrupted
30+
*/
31+
HttpResponse<String> execute(HttpStepRequest request) throws IOException, InterruptedException;
32+
}

0 commit comments

Comments
 (0)