Skip to content

Commit 8fb2ba3

Browse files
committed
Initial
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <[email protected]>
1 parent b2a4c4f commit 8fb2ba3

9 files changed

+472
-5
lines changed

.DEREK.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
redirect: https://raw.githubusercontent.com/openfaas/faas/master/.DEREK.yml

LICENSE

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
Annotated portions of this project are licensed under the OpenFaaS Pro
2+
commercial license, for which a license is required to use the software.
3+
4+
EULA: https://github.com/openfaas/faas/blob/master/pro/EULA.md
5+
6+
The remainder of the source code is licensed under the MIT license.
7+
18
MIT License
29

3-
Copyright (c) 2023 OpenFaaS
10+
Copyright (c) 2017-2023 OpenFaaS Ltd
411

512
Permission is hereby granted, free of charge, to any person obtaining a copy
613
of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +16,14 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
916
copies of the Software, and to permit persons to whom the Software is
1017
furnished to do so, subject to the following conditions:
1118

12-
The above copyright notice and this permission notice shall be included in all
13-
copies or substantial portions of the Software.
19+
The above copyright notice and this permission notice shall be included in
20+
all copies or substantial portions of the Software.
1421

1522
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1623
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1724
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1825
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1926
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
27+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28+
THE SOFTWARE.
29+

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## go-sdk
2+
3+
A lightweight Go SDK for use within OpenFaaS functions and to control the OpenFaaS gateway.
4+
5+
For use within any Go code (not just OpenFaaS Functions):
6+
7+
* Client - A client for the OpenFaaS REST API
8+
9+
For use within functions:
10+
11+
* ReadSecret() - Read a named secret from within an OpenFaaS Function
12+
* ReadSecrets() - Read all available secrets returning a queryable map
13+
14+
License: MIT

client.go

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package sdk
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"path/filepath"
12+
13+
"github.com/openfaas/faas-provider/auth"
14+
"github.com/openfaas/faas-provider/types"
15+
)
16+
17+
// Client is used to manage OpenFaaS functions
18+
type Client struct {
19+
GatewayURL *url.URL
20+
Client *http.Client
21+
Credentials *auth.BasicAuthCredentials
22+
}
23+
24+
// NewClient creates an Client for managing OpenFaaS
25+
func NewClient(gatewayURL *url.URL, credentials *auth.BasicAuthCredentials, client *http.Client) *Client {
26+
return &Client{
27+
GatewayURL: gatewayURL,
28+
Client: http.DefaultClient,
29+
Credentials: credentials,
30+
}
31+
}
32+
33+
// GetNamespaces get openfaas namespaces
34+
func (s *Client) GetNamespaces() ([]string, error) {
35+
u := s.GatewayURL
36+
namespaces := []string{}
37+
u.Path = "/system/namespaces"
38+
39+
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
40+
if err != nil {
41+
return namespaces, fmt.Errorf("unable to create request: %s, error: %w", u.String(), err)
42+
}
43+
44+
if s.Credentials != nil {
45+
req.SetBasicAuth(s.Credentials.User, s.Credentials.Password)
46+
}
47+
48+
res, err := s.Client.Do(req)
49+
if err != nil {
50+
return namespaces, fmt.Errorf("unable to make request: %w", err)
51+
}
52+
53+
if res.Body != nil {
54+
defer res.Body.Close()
55+
}
56+
57+
bytesOut, err := ioutil.ReadAll(res.Body)
58+
if err != nil {
59+
return namespaces, err
60+
}
61+
62+
if res.StatusCode == http.StatusUnauthorized {
63+
return namespaces, fmt.Errorf("check authorization, status code: %d", res.StatusCode)
64+
}
65+
66+
if len(bytesOut) == 0 {
67+
return namespaces, nil
68+
}
69+
70+
if err := json.Unmarshal(bytesOut, &namespaces); err != nil {
71+
return namespaces, fmt.Errorf("unable to marshal to JSON: %s, error: %w", string(bytesOut), err)
72+
}
73+
74+
return namespaces, err
75+
}
76+
77+
// GetFunctions lists all functions
78+
func (s *Client) GetFunctions(namespace string) ([]types.FunctionStatus, error) {
79+
u := s.GatewayURL
80+
81+
u.Path = "/system/functions"
82+
83+
if len(namespace) > 0 {
84+
query := u.Query()
85+
query.Set("namespace", namespace)
86+
u.RawQuery = query.Encode()
87+
}
88+
89+
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
90+
if err != nil {
91+
return []types.FunctionStatus{}, fmt.Errorf("unable to create request for %s, error: %w", u.String(), err)
92+
}
93+
94+
if s.Credentials != nil {
95+
req.SetBasicAuth(s.Credentials.User, s.Credentials.Password)
96+
}
97+
98+
res, err := s.Client.Do(req)
99+
if err != nil {
100+
return []types.FunctionStatus{}, fmt.Errorf("unable to make HTTP request: %w", err)
101+
}
102+
103+
if res.Body != nil {
104+
defer res.Body.Close()
105+
}
106+
107+
body, _ := ioutil.ReadAll(res.Body)
108+
109+
functions := []types.FunctionStatus{}
110+
if err := json.Unmarshal(body, &functions); err != nil {
111+
return []types.FunctionStatus{},
112+
fmt.Errorf("unable to unmarshal value: %q, error: %w", string(body), err)
113+
}
114+
115+
return functions, nil
116+
}
117+
118+
// ScaleFunction scales a function to a number of replicas
119+
func (s *Client) ScaleFunction(ctx context.Context, functionName, namespace string, replicas uint64) error {
120+
121+
scaleReq := types.ScaleServiceRequest{
122+
ServiceName: functionName,
123+
Replicas: replicas,
124+
}
125+
126+
var err error
127+
128+
bodyBytes, _ := json.Marshal(scaleReq)
129+
bodyReader := bytes.NewReader(bodyBytes)
130+
131+
u := s.GatewayURL
132+
133+
functionPath := filepath.Join("/system/scale-function", functionName)
134+
if len(namespace) > 0 {
135+
query := u.Query()
136+
query.Set("namespace", namespace)
137+
u.RawQuery = query.Encode()
138+
}
139+
140+
u.Path = functionPath
141+
142+
req, err := http.NewRequest(http.MethodPost, u.String(), bodyReader)
143+
if err != nil {
144+
return fmt.Errorf("cannot connect to OpenFaaS on URL: %s, error: %s", u.String(), err)
145+
}
146+
147+
if s.Credentials != nil {
148+
req.SetBasicAuth(s.Credentials.User, s.Credentials.Password)
149+
}
150+
res, err := http.DefaultClient.Do(req)
151+
if err != nil {
152+
return fmt.Errorf("cannot connect to OpenFaaS on URL: %s, error: %s", s.GatewayURL, err)
153+
154+
}
155+
156+
if res.Body != nil {
157+
defer res.Body.Close()
158+
}
159+
160+
switch res.StatusCode {
161+
case http.StatusAccepted, http.StatusOK, http.StatusCreated:
162+
break
163+
164+
case http.StatusNotFound:
165+
return fmt.Errorf("function %s not found", functionName)
166+
167+
case http.StatusUnauthorized:
168+
return fmt.Errorf("unauthorized action, please setup authentication for this server")
169+
170+
default:
171+
var err error
172+
bytesOut, err := ioutil.ReadAll(res.Body)
173+
if err != nil {
174+
return err
175+
}
176+
177+
return fmt.Errorf("server returned unexpected status code %d, message: %q", res.StatusCode, string(bytesOut))
178+
}
179+
return nil
180+
}

client_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package sdk
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"testing"
8+
)
9+
10+
func TestSdk_GetNamespaces_TwoNamespaces(t *testing.T) {
11+
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
12+
13+
rw.Write([]byte(`["openfaas-fn","dev"]`))
14+
}))
15+
16+
sU, _ := url.Parse(s.URL)
17+
18+
client := NewClient(sU, nil, http.DefaultClient)
19+
ns, err := client.GetNamespaces()
20+
if err != nil {
21+
t.Fatalf("wanted no error, but got: %s", err)
22+
}
23+
want := 2
24+
if len(ns) != want {
25+
t.Fatalf("want %d namespaces, got: %d", want, len(ns))
26+
}
27+
wantNS := []string{"openfaas-fn", "dev"}
28+
gotNS := 0
29+
30+
for _, n := range ns {
31+
for _, w := range wantNS {
32+
if n == w {
33+
gotNS++
34+
}
35+
}
36+
}
37+
if gotNS != len(wantNS) {
38+
t.Fatalf("want %d namespaces, got: %d", len(wantNS), gotNS)
39+
}
40+
}
41+
42+
func TestSdk_GetNamespaces_NoNamespaces(t *testing.T) {
43+
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
44+
45+
rw.Write([]byte(`[]`))
46+
}))
47+
48+
sU, _ := url.Parse(s.URL)
49+
50+
client := NewClient(sU, nil, http.DefaultClient)
51+
ns, err := client.GetNamespaces()
52+
if err != nil {
53+
t.Fatalf("wanted no error, but got: %s", err)
54+
}
55+
want := 0
56+
if len(ns) != want {
57+
t.Fatalf("want %d namespaces, got: %d", want, len(ns))
58+
}
59+
wantNS := []string{}
60+
gotNS := 0
61+
62+
for _, n := range ns {
63+
for _, w := range wantNS {
64+
if n == w {
65+
gotNS++
66+
}
67+
}
68+
}
69+
if gotNS != len(wantNS) {
70+
t.Fatalf("want %d namespaces, got: %d", len(wantNS), gotNS)
71+
}
72+
}

go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/openfaas/go-sdk
2+
3+
go 1.19
4+
5+
require github.com/openfaas/faas-provider v0.19.1
6+
7+
require github.com/google/go-cmp v0.5.9 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
2+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
github.com/openfaas/faas-provider v0.19.1 h1:xH8lTWabfDZwzIvC0u1AO48ghD3BNw6Vo231DLqTeI0=
4+
github.com/openfaas/faas-provider v0.19.1/go.mod h1:Farrp+9Med8LeK3aoYpqplMP8f5ebTILbCSLg2LPLZk=

secrets.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package sdk
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path"
7+
"strings"
8+
)
9+
10+
// ReadSecrets reads a single secrets from /var/openfaas/secrets or from
11+
// the environment "secret_mount_path" if set.
12+
func ReadSecret(key string) (string, error) {
13+
14+
readPath := getPath(key)
15+
secretBytes, readErr := os.ReadFile(readPath)
16+
if readErr != nil {
17+
return "", fmt.Errorf("unable to read secret: %s, error: %s", readPath, readErr)
18+
}
19+
val := strings.TrimSpace(string(secretBytes))
20+
return val, nil
21+
}
22+
23+
// ReadSecrets reads all secrets from /var/openfaas/secrets or from
24+
// the environment "secret_mount_path" if set.
25+
// The results are returned in a map of key/value pairs.
26+
func ReadSecrets() (SecretMap, error) {
27+
28+
values := map[string]string{}
29+
secretMap := newSecretMap(values)
30+
base := getPath("")
31+
32+
files, err := os.ReadDir(base)
33+
if err != nil {
34+
return secretMap, err
35+
}
36+
37+
for _, file := range files {
38+
val, err := ReadSecret(file.Name())
39+
if err != nil {
40+
return secretMap, err
41+
}
42+
values[file.Name()] = val
43+
}
44+
45+
return secretMap, nil
46+
}
47+
48+
func newSecretMap(values map[string]string) SecretMap {
49+
return SecretMap{
50+
values: values,
51+
}
52+
}
53+
54+
func getPath(key string) string {
55+
basePath := "/var/openfaas/secrets/"
56+
if len(os.Getenv("secret_mount_path")) > 0 {
57+
basePath = os.Getenv("secret_mount_path")
58+
}
59+
return path.Join(basePath, key)
60+
}
61+
62+
type SecretMap struct {
63+
values map[string]string
64+
}
65+
66+
func (s *SecretMap) Get(key string) (string, error) {
67+
val, ok := s.values[key]
68+
if !ok {
69+
return "", fmt.Errorf("secret %s not found", key)
70+
}
71+
return val, nil
72+
}
73+
74+
func (s *SecretMap) Exists(key string) bool {
75+
_, ok := s.values[key]
76+
return ok
77+
}

0 commit comments

Comments
 (0)