Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:
strategy:
matrix:
go:
- 1.17.x
- 1.18.x
- 1.19.x
os:
- ubuntu-latest
- macos-latest
Expand All @@ -33,11 +33,11 @@ jobs:
run: go build -v ./...
- name: Test (with coverprofile)
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.17.x'
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.18.x'
- name: Test (no coverprofile)
run: go test -v -race ./...
if: matrix.os != 'ubuntu-latest' || matrix.go != '1.17.x'
if: matrix.os != 'ubuntu-latest' || matrix.go != '1.18.x'
# Upload coverage report only in one case of the matrix
- name: Upload coverage report
uses: codecov/codecov-action@v1
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.17.x'
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.18.x'
6 changes: 3 additions & 3 deletions .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
# Ensure we're on Go 1.17
go-version: '1.17.x'
# Ensure we're on Go 1.18
go-version: '1.18.x'
# Run golint-ci
- name: Run golangci-lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.25.1
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.51.1
./bin/golangci-lint run
13 changes: 8 additions & 5 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
# Allow 10m, within actions it might take a lot
timeout: 10m

# Ignore the beautiful, beautiful Astarte mock file
skip-files:
- client/astarte_mock.go
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
Expand Down Expand Up @@ -116,25 +118,26 @@ linters-settings:
allow-separated-leading-comment: false

linters:
disable-all: true
enable:
- bodyclose
# - bodyclose # too many false positives (we close the response in Parse, but generate it in Run)
- dupl
- funlen
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- golint
- revive
- gofmt
- goimports
- goprintffuncname
- gosec
#- lll
- maligned
# - fieldalignment # Disabled temporairly
- misspell
- prealloc
- scopelint
- exportloopref
- unconvert
- unparam

Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.17.8
golang 1.18.8
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
module github.com/astarte-platform/astarte-go

go 1.17
go 1.18

require (
github.com/cristalhq/jwt/v3 v3.1.0
github.com/google/uuid v1.3.0
github.com/iancoleman/orderedmap v0.2.0
github.com/tidwall/gjson v1.14.4
)

require github.com/smartystreets/goconvey v1.7.2 // indirect

require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,26 @@ github.com/cristalhq/jwt/v3 v3.1.0 h1:iLeL9VzB0SCtjCy9Kg53rMwTcrNm+GHyVcz2eUujz6
github.com/cristalhq/jwt/v3 v3.1.0/go.mod h1:XOnIXst8ozq/esy5N1XOlSyQqBd+84fxJ99FK+1jgL8=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
245 changes: 245 additions & 0 deletions newclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright © 2023 SECO Mind srl
//
// 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 newclient

import (
"net/http"
"net/url"
"time"

"github.com/astarte-platform/astarte-go/misc"
)

type Client struct {
baseURL *url.URL
appEngineURL *url.URL
housekeepingURL *url.URL
pairingURL *url.URL
realmManagementURL *url.URL
userAgent string

httpClient *http.Client
token string
}

type clientOption = func(c *Client) error

// Finally, generics (actually, type constraints)
type privateKeyProvider interface {
string | []byte
}

// The New function creates a new Astarte API client.
// If no options are specified, the following is assumed:
// - standard Astarte URL hierarchy
// - standard HTTP client
// - no JWT token (no call will be authorized)
// - "astarte-go" as user agent
// A production-ready client may be created using e.g.:
// `client.New(client.WithBaseUrl("api.your-astarte.org"), client.WithToken("YOUR_JWT_TOKEN"))``
func New(options ...clientOption) (*Client, error) {
// We start with a client with bare zero-valued fields
c := &Client{}

// Then we modify it according to user-provided options...
for _, f := range options {
err := f(c)
if err != nil {
return c, err
}
}

// ... and check if the result is valid
if err := validate(c); err != nil {
return c, err
}

// Finally, we add just a sprinkle of defaults and a new Client is born!
return setDefaults(c), nil
}

// The WithAppengineURL function allows to specify an
// AppEngine URL different from the standard one (e.g. http://localhost:4000).
// This is not recommendend in production.
func WithAppengineURL(appEngineURL string) clientOption {
return func(c *Client) error {
appengine, err := url.Parse(appEngineURL)
if err != nil {
return err
}
c.appEngineURL = appengine
return nil
}
}

// The WithHousekeepingURL function allows to specify an
// Housekeeping URL different from the standard one (e.g. http://localhost:4001).
// This is not recommendend in production.
func WithHousekeepingURL(housekeepingURL string) clientOption {
return func(c *Client) error {
housekeeping, err := url.Parse(housekeepingURL)
if err != nil {
return err
}
c.housekeepingURL = housekeeping
return nil
}
}

// The WithPairingURL function allows to specify an
// Pairing URL different from the standard one (e.g. http://localhost:4002).
// This is not recommendend in production.
func WithPairingURL(pairingURL string) clientOption {
return func(c *Client) error {
// check that it's a valid URL
pairing, err := url.Parse(pairingURL)
if err != nil {
return err
}
c.pairingURL = pairing
return nil
}
}

// The WithRealmManagementURL function allows to specify an
// RealmManagement URL different from the standard one (e.g. http://localhost:4003).
// This is not recommendend in production.
func WithRealmManagementURL(realmManagementURL string) clientOption {
return func(c *Client) error {
realmManagement, err := url.Parse(realmManagementURL)
if err != nil {
return err
}
c.realmManagementURL = realmManagement
return nil
}
}

// The WithBaseURL function allows to specify the Astarte
// base URL (e.g. api.your-astarte.org)
func WithBaseURL(baseURL string) clientOption {
return func(c *Client) error {
base, err := url.Parse(baseURL)
if err != nil {
return err
}
c.baseURL = base

return nil
}
}

// The WithHTTPClient function allows to specify an httpClient
// with custom options, e.g. different timeout, or skipTLSVerify
func WithHTTPClient(httpClient *http.Client) clientOption {
return func(c *Client) error {
c.httpClient = httpClient
return nil
}
}

// The WithToken function allows to specify a JWT
// token that the client will use to interact with Astarte.
func WithToken(token string) clientOption {
return func(c *Client) error {
c.token = token
return nil
}
}

// The WithUserAgent function allows to specify the User Agent
// that the client will use when making http requests.
func WithUserAgent(userAgent string) clientOption {
return func(c *Client) error {
c.userAgent = userAgent
return nil
}
}

// The WithPrivateKey function allows to specify a realm private key,
// used internally to generate a valid JWT token to all Astarte APIs with no expiry.
// The client will use that token to interact with Astarte.
func WithPrivateKey[T privateKeyProvider](privateKey T) clientOption {
return WithPrivateKeyWithTTL(privateKey, 0)
}

// The WithPrivateKey function allows to specify a realm private key,
// used internally to generate a valid JWT token to all Astarte APIs with a specified expiry (in seconds).
// The client will use that token to interact with Astarte.
func WithPrivateKeyWithTTL[T privateKeyProvider](privateKey T, ttlSeconds int64) clientOption {
// Add all types
servicesAndClaims := map[misc.AstarteService][]string{
misc.AppEngine: {},
misc.Channels: {},
misc.Flow: {},
misc.Housekeeping: {},
misc.Pairing: {},
misc.RealmManagement: {},
}
return WithPrivateKeyWithClaimsWithTTL(privateKey, servicesAndClaims, 0)
}

// The WithPrivateKey function allows to specify a realm private key,
// used internally to generate a valid JWT token with a given set of Astarte claims and
// a specified expiry (in seconds).
// The client will use that token to interact with Astarte.
func WithPrivateKeyWithClaimsWithTTL[T privateKeyProvider](privateKey T, claims map[misc.AstarteService][]string, ttlSeconds int64) clientOption {
return func(c *Client) error {
// Golang I hate you so much
switch k := any(privateKey).(type) {
case string:
var err error
c.token, err = misc.GenerateAstarteJWTFromKeyFile(k, claims, ttlSeconds)
return err
case []byte:
var err error
c.token, err = misc.GenerateAstarteJWTFromPEMKey(k, claims, ttlSeconds)
return err
default:
return ErrNoPrivateKeyProvided
}
}
}

func validate(c *Client) error {
if c.baseURL != nil && (c.appEngineURL != nil || c.realmManagementURL != nil || c.housekeepingURL != nil || c.pairingURL != nil) {
return ErrConflictingUrls
}
if c.baseURL == nil && c.appEngineURL == nil && c.realmManagementURL == nil && c.housekeepingURL == nil && c.pairingURL == nil {
return ErrNoUrlsProvided
}
return nil
}

func setDefaults(c *Client) *Client {
if c.httpClient == nil {
c.httpClient = &http.Client{
Timeout: time.Second * 30,
}

}
if c.userAgent == "" {
c.userAgent = "astarte-go"
}

if c.baseURL != nil {
c.appEngineURL, _ = url.Parse(c.baseURL.String()+"/appengine")
c.housekeepingURL, _ = url.Parse(c.baseURL.String()+"/housekeeping")
c.pairingURL, _ = url.Parse(c.baseURL.String()+"/pairing")
c.realmManagementURL, _ = url.Parse(c.baseURL.String()+"/realmmanagement")
}

return c
}
Loading