diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dd01830 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,15 @@ +name: build + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Install Opctl + run: curl -L https://github.com/opctl/opctl/releases/download/0.1.45/opctl0.1.45.linux.tgz | sudo tar -xzv -C /usr/local/bin + + - name: Build + run: opctl run build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ad0969 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Jetbrains +.idea/ + +# opctl +*args*.yml \ No newline at end of file diff --git a/.opspec/build/op.yml b/.opspec/build/op.yml new file mode 100644 index 0000000..da98b54 --- /dev/null +++ b/.opspec/build/op.yml @@ -0,0 +1,10 @@ +name: build +description: builds the op +run: + serial: + - op: + ref: github.com/opspec-pkgs/_.op.bootstrap#4.1.1 + inputs: + srcDir: $(../..) + - op: + ref: test \ No newline at end of file diff --git a/.opspec/e2eTest/op.yml b/.opspec/e2eTest/op.yml new file mode 100644 index 0000000..d038fbc --- /dev/null +++ b/.opspec/e2eTest/op.yml @@ -0,0 +1,35 @@ +name: e2eTest +run: + serial: + - op: + ref: $(../../) + inputs: + headers: + Content-Type: + - text/html + url: https://google.com + method: GET + outputs: + body: + headers: + protocol: + statusCode: + - container: + image: + ref: alpine + cmd: + - sh + - -ce + - | + echo $(protocol) $(statusCode) + echo 'Content-Type' + echo $(headers[Content-Type][0]) + echo 'headers' + cat /headers + echo "" + echo "body" + cat /body + sleep 1 + files: + /body: $(body) + /headers: $(headers) \ No newline at end of file diff --git a/.opspec/provision-image/op.yml b/.opspec/provision-image/op.yml new file mode 100644 index 0000000..d4a3e87 --- /dev/null +++ b/.opspec/provision-image/op.yml @@ -0,0 +1,44 @@ +name: provision-image +description: | + Provisions image for the op to [opspec-pkgs image registry](https://github.com/orgs/opspec-pkgs/packages) w/ name `$(opDotYml.name):${opDotYml.version}`. + + > Before first use, a Dockerfile MUST be added to `.opspec/provision-image` +inputs: + githubUsername: + string: + constraints: { minLength: 1 } + description: user for logging in to github + githubAccessToken: + string: + constraints: { minLength: 1 } + description: accessToken for logging in to github + isSecret: true +run: + serial: + - op: + ref: github.com/opspec-pkgs/yaml.parse#1.0.0 + inputs: + yaml: $(../../op.yml) + outputs: + result: $(opDotYml) + - container: + image: { ref: 'alpine:3.6' } + cmd: [sh, -ce, 'echo -n ghcr.io/opspec-pkgs/${opName#*/*/}:$(opDotYml.version) > /imageName'] + envVars: + opName: $(opDotYml.name) + files: + /imageName: $(imageName) + - op: + ref: github.com/opspec-pkgs/docker.image.build#2.1.0 + inputs: + dockerfile: $(./Dockerfile) + imageName: + outputs: { imageTar } + - op: + ref: github.com/opspec-pkgs/docker.image.push#2.1.0 + inputs: + imageTar: + imageName: + registry: ghcr.io + username: $(githubUsername) + password: $(githubAccessToken) diff --git a/.opspec/release/op.yml b/.opspec/release/op.yml new file mode 100644 index 0000000..b03531c --- /dev/null +++ b/.opspec/release/op.yml @@ -0,0 +1,43 @@ +name: release +description: releases the op +inputs: + githubUsername: + string: + constraints: { minLength: 1 } + description: user for logging in to github + githubAccessToken: + string: + constraints: { minLength: 1 } + description: accessToken for logging in to github + isSecret: true +run: + serial: + - op: + ref: build + - op: + ref: github.com/opspec-pkgs/yaml.parse#1.0.0 + inputs: + yaml: $(../../op.yml) + outputs: + result: $(opDotYml) + - op: + ref: github.com/opspec-pkgs/git.repo.resolve-commit#1.0.2 + inputs: { dotGitDir: $(../../.git) } + outputs: { commit: } + - container: + image: { ref: 'alpine:3.6' } + cmd: [sh, -ce, 'echo -n ${opName#*/*/} > /opRepo'] + envVars: + opName: $(opDotYml.name) + files: + /opRepo: $(opRepo) + - op: + ref: github.com/opspec-pkgs/github.release.create#1.1.0 + inputs: + owner: opspec-pkgs + repo: $(opRepo) + loginUsername: $(githubUsername) + loginPassword: $(githubAccessToken) + tag: $(opDotYml.version) + commitish: $(commit) + name: $(opDotYml.version) \ No newline at end of file diff --git a/.opspec/test/op.yml b/.opspec/test/op.yml new file mode 100644 index 0000000..5dc431e --- /dev/null +++ b/.opspec/test/op.yml @@ -0,0 +1,8 @@ +name: test +description: tests the op +run: + op: + ref: github.com/opspec-pkgs/opctl.op.validate#1.0.0 + inputs: + opctlVersion: 0.1.53 + op: $(../..) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e92fd9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Change Log + +All notable changes will be documented in this file in accordance with +[![keepachangelog 1.0.0](https://img.shields.io/badge/keepachangelog-1.0.0-brightgreen.svg)](http://keepachangelog.com/en/1.0.0/) + +## \[Unreleased] \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..60761a7 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @opspec-pkgs/maintainers diff --git a/README.md b/README.md new file mode 100644 index 0000000..72ddece --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +[![Build Status](https://github.com/opspec-pkgs/http.send/workflows/build/badge.svg?branch=main)](https://github.com/opspec-pkgs/http.send/actions?query=workflow%3Abuild+branch%3Amain) + +icon + +# Problem statement + +Sends an http request + +# Example usage + +## Visualize + +```shell +opctl ui github.com/opspec-pkgs/http.send#1.0.0 +``` + +## Run + +``` +opctl run github.com/opspec-pkgs/http.send#1.0.0 +``` + +## Compose + +```yaml +op: + ref: github.com/opspec-pkgs/http.send#1.0.0 + inputs: + method: # 👈 required; provide a value + url: # 👈 required; provide a value + ## uncomment to override defaults + # body: "[object Object]" + # headers: [object Object] + outputs: + body: + headers: + protocol: + statusCode: +``` + +# Support + +join us on +[![Slack](https://img.shields.io/badge/slack-opctl-E01563.svg)](https://join.slack.com/t/opctl/shared_invite/zt-51zodvjn-Ul_UXfkhqYLWZPQTvNPp5w) +or +[open an issue](https://github.com/opspec-pkgs/http.send/issues) + +# Releases + +releases are versioned according to +[![semver 2.0.0](https://img.shields.io/badge/semver-2.0.0-brightgreen.svg)](http://semver.org/spec/v2.0.0.html) +and [tagged](https://git-scm.com/book/en/v2/Git-Basics-Tagging); see +[CHANGELOG.md](CHANGELOG.md) for release notes + +# Contributing + +see +[project/CONTRIBUTING.md](https://github.com/opspec-pkgs/project/blob/main/CONTRIBUTING.md) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..71f42ed --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/opspec-pkgs/http.send + +go 1.23.2 diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..e69de29 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..7851988 --- /dev/null +++ b/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/internal/inout/doc.go b/internal/inout/doc.go new file mode 100644 index 0000000..9746895 --- /dev/null +++ b/internal/inout/doc.go @@ -0,0 +1,2 @@ +// package inout is used to process op input to an http request and http response to op output +package inout \ No newline at end of file diff --git a/internal/inout/getHeaders.go b/internal/inout/getHeaders.go new file mode 100644 index 0000000..4183824 --- /dev/null +++ b/internal/inout/getHeaders.go @@ -0,0 +1,54 @@ +package inout + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" +) + +func getHeaders() ( + map[string][]string, + error, +) { + headersJSONBytes, err := os.ReadFile(HeadersFilePath) + if err != nil { + return nil, err + } + + rawHeaders := map[string][]interface{}{} + if err := json.Unmarshal(headersJSONBytes, &rawHeaders); err != nil { + return nil, err + } + + headersMap := http.Header{} + for k, vSlice := range rawHeaders { + for _, rawV := range vSlice { + v, err := convertToString(rawV) + if err != nil { + return nil, err + } + + headersMap.Add(k, v) + } + } + + return headersMap, nil +} + +func convertToString( + in interface{}, +) (string, error) { + if s, ok := in.(string); ok { + return s, nil + } else if b, ok := in.(bool); ok { + return strconv.FormatBool(b), nil + } else if f, ok := in.(float64); ok { + return strconv.FormatFloat(f, 'f', -1, 64), nil + } else if i, ok := in.(int64); ok { + return strconv.FormatInt(i, 10), nil + } + + return "", fmt.Errorf("%+v not a valid header value", in) +} diff --git a/internal/inout/getRequest.go b/internal/inout/getRequest.go new file mode 100644 index 0000000..ee14269 --- /dev/null +++ b/internal/inout/getRequest.go @@ -0,0 +1,46 @@ +package inout + +import ( + "net/http" + "os" +) + +func GetRequest() ( + *http.Request, + error, +) { + methodBytes, err := os.ReadFile(MethodFilePath) + if err != nil { + return nil, err + } + + urlBytes, err := os.ReadFile(URLFilePath) + if err != nil { + return nil, err + } + + reqBody, err := os.Open(BodyFilePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest( + string(methodBytes), + string(urlBytes), + reqBody, + ) + if err != nil { + return nil, err + } + + if req.ContentLength == 0 { + req.Body = nil + } + + req.Header, err = getHeaders() + if err != nil { + return nil, err + } + + return req, nil +} diff --git a/internal/inout/paths.go b/internal/inout/paths.go new file mode 100644 index 0000000..dd0346d --- /dev/null +++ b/internal/inout/paths.go @@ -0,0 +1,10 @@ +package inout + +const ( + BodyFilePath = "/body.txt" + HeadersFilePath = "/headers.json" + MethodFilePath = "/method.txt" + ProtocolFilePath = "/protocol.txt" + StatusCodeFilePath = "/statusCode.txt" + URLFilePath = "/url.txt" +) \ No newline at end of file diff --git a/internal/inout/setBody.go b/internal/inout/setBody.go new file mode 100644 index 0000000..c48c485 --- /dev/null +++ b/internal/inout/setBody.go @@ -0,0 +1,20 @@ +package inout + +import ( + "io" + "os" +) + +func setBody( + body io.ReadCloser, +) error { + bodyFile, err := os.Create(BodyFilePath) + if err != nil { + return err + } + defer bodyFile.Close() + + _, err = io.Copy(bodyFile, body) + defer body.Close() + return err +} diff --git a/internal/inout/setHeaders.go b/internal/inout/setHeaders.go new file mode 100644 index 0000000..0d84635 --- /dev/null +++ b/internal/inout/setHeaders.go @@ -0,0 +1,22 @@ +package inout + +import ( + "encoding/json" + "net/http" + "os" +) + +func setHeaders( + headers http.Header, +) error { + headersJSONBytes, err := json.Marshal(headers) + if err != nil { + return err + } + + return os.WriteFile( + HeadersFilePath, + headersJSONBytes, + 0644, + ) +} diff --git a/internal/inout/setResponse.go b/internal/inout/setResponse.go new file mode 100644 index 0000000..66d080b --- /dev/null +++ b/internal/inout/setResponse.go @@ -0,0 +1,27 @@ +package inout + +import ( + "net/http" + "os" + "strconv" +) + +func SetResponse( + resp *http.Response, +) error { + + if err := setBody(resp.Body); err != nil { + return err + } + if err := setHeaders(resp.Header); err != nil { + return err + } + if err := os.WriteFile(ProtocolFilePath, []byte(resp.Proto), 0644); err != nil { + return err + } + if err := os.WriteFile(StatusCodeFilePath, []byte(strconv.Itoa(resp.StatusCode)), 0644); err != nil { + return err + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f9c69cb --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "net/http" + + "github.com/opspec-pkgs/http.send/internal/inout" +) + +func main() { + + req, err := inout.GetRequest() + if err != nil { + panic(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + + if err := inout.SetResponse(resp); err != nil { + panic(err) + } + +} \ No newline at end of file diff --git a/op.yml b/op.yml new file mode 100644 index 0000000..4b5c468 --- /dev/null +++ b/op.yml @@ -0,0 +1,58 @@ +description: Sends an http request +name: github.com/opspec-pkgs/http.send +inputs: + body: + description: body/content of the request + string: + default: {} + headers: + description: headers of the request + object: + constraints: + patternProperties: + ".+": + type: array + default: {} + method: + description: method of the request e.g. GET, HEAD, POST, PUT, DELETE, etc... + string: + constraints: + minLength: 1 + url: + description: url of the request + string: + constraints: + format: uri +outputs: + body: + description: body/content of the response + string: + default: {} + headers: + description: headers of the response + object: {} + protocol: + description: protocol of the response e.g. HTTP/1.1 + string: {} + statusCode: + description: status code of the response + number: {} +run: + container: + cmd: + - go + - run + - /src/main.go + dirs: + /src: $(./) + image: + ref: golang:1.23 + files: + /body.txt: $(body) + /headers.json: $(headers) + /method.txt: $(method) + /protocol.txt: $(protocol) + /statusCode.txt: $(statusCode) + /url.txt: $(url) + workDir: /src +version: 1.0.0