Skip to content

Commit

Permalink
feat: Expose structured API response errors (#531)
Browse files Browse the repository at this point in the history
## Motivation

`sdk.Client` methods which interact with Nobl9 API currently return only
generic text based errors.
This obstructs the relevant details from the user. Furthermore, the
current default text format for these errors is fairly poor and doesn't
contain details like endpoint path.

## Summary

- Added `sdk.APIError` which is a structured error that contains all the
relevant API error details as well as produces an improved (over the
previous state) text representation.
- Improved docs.
- Removed PlantUML in favour of Mermaid, since it's much easier to use
as it doesn't require Java and renders natively in Markdown.

## Testing

Covered by both unit and end-to-end tests.

## Release Notes

`sdk.Client` API calls now can return `sdk.APIError`, which is a
structured error providing details of the error returned by Nobl9's API.
  • Loading branch information
nieomylnieja authored Oct 1, 2024
1 parent 621dce5 commit 45a5c1c
Show file tree
Hide file tree
Showing 18 changed files with 407 additions and 273 deletions.
22 changes: 3 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ check/format:
$(call _print_check_step,Checking if files are formatted)
./scripts/check-formatting.sh

.PHONY: generate generate/code generate/examples generate/plantuml
.PHONY: generate generate/code generate/examples
## Auto generate files.
generate: generate/code generate/examples generate/plantuml
generate: generate/code generate/examples

## Generate Golang code.
generate/code:
Expand All @@ -127,22 +127,6 @@ generate/examples:
echo "Generating examples..."
go run internal/cmd/examplegen/main.go

PLANTUML_JAR_URL := https://sourceforge.net/projects/plantuml/files/plantuml.jar/download
PLANTUML_JAR := $(BIN_DIR)/plantuml.jar
DIAGRAMS_PATH ?= .

## Generate PNG diagrams from PlantUML files.
generate/plantuml: $(PLANTUML_JAR)
for path in $$(find $(DIAGRAMS_PATH) -name "*.puml" -type f); do \
echo "Generating PNG file(s) for $$path"; \
java -jar $(PLANTUML_JAR) -tpng $$path; \
done

# If the plantuml.jar file isn't already present, download it.
$(PLANTUML_JAR):
echo "Downloading PlantUML JAR..."
curl -sSfL $(PLANTUML_JAR_URL) -o $(PLANTUML_JAR)

.PHONY: format format/go format/cspell
## Format files.
format: format/go format/cspell
Expand All @@ -151,7 +135,7 @@ format: format/go format/cspell
format/go:
echo "Formatting Go files..."
$(call _ensure_installed,binary,goimports)
go fmt ./...
gofmt -w -l -s .
$(BIN_DIR)/goimports -local=github.com/nobl9/nobl9-go -w .

## Format cspell config file.
Expand Down
241 changes: 150 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ Legend:

1. [Installation](#installation)
2. [Examples](#examples)
3. [Repository structure](#repository-structure)
4. [Contributing](#contributing)
3. [Usage](#usage)
4. [Repository structure](#repository-structure)
5. [Contributing](#contributing)

# Installation

Expand All @@ -54,105 +55,155 @@ go get github.com/nobl9/nobl9-go
## Basic usage

<!-- markdownlint-disable MD013 -->

```go
package main

import (
"context"
"encoding/json"
"fmt"
"log"

"github.com/nobl9/nobl9-go/manifest"
"github.com/nobl9/nobl9-go/manifest/v1alpha"
"github.com/nobl9/nobl9-go/manifest/v1alpha/project"
"github.com/nobl9/nobl9-go/manifest/v1alpha/service"
"github.com/nobl9/nobl9-go/sdk"
objectsV1 "github.com/nobl9/nobl9-go/sdk/endpoints/objects/v1"
"context"
"encoding/json"
"fmt"
"log"

"github.com/nobl9/nobl9-go/manifest"
"github.com/nobl9/nobl9-go/manifest/v1alpha"
"github.com/nobl9/nobl9-go/manifest/v1alpha/project"
"github.com/nobl9/nobl9-go/manifest/v1alpha/service"
"github.com/nobl9/nobl9-go/sdk"
objectsV1 "github.com/nobl9/nobl9-go/sdk/endpoints/objects/v1"
)

func main() {
ctx := context.Background()

// Create client.
client, err := sdk.DefaultClient()
if err != nil {
log.Fatalf("failed to create sdk client, err: %v", err)
}

// Read from file, url or glob pattern.
objects, err := sdk.ReadObjects(ctx, "./project.yaml")
if err != nil {
log.Fatalf("failed to read project.yaml file, err: %v", err)
}
// Use manifest.FilterByKind to extract specific objects from the manifest.Object slice.
myProject := manifest.FilterByKind[project.Project](objects)[0]
// Define objects in code.
myService := service.New(
service.Metadata{
Name: "my-service",
DisplayName: "My Service",
Project: myProject.GetName(),
Labels: v1alpha.Labels{
"team": []string{"green", "orange"},
"region": []string{"eu-central-1"},
},
},
service.Spec{
Description: "Example service",
},
)
objects = append(objects, myService)

// Verify the objects.
if errs := manifest.Validate(objects); len(errs) > 0 {
log.Fatalf("service validation failed, errors: %v", errs)
}

// Apply the objects.
if err = client.Objects().V1().Apply(ctx, objects); err != nil {
log.Fatalf("failed to apply objects, err: %v", err)
}

// Get the applied resources.
services, err := client.Objects().V1().GetV1alphaServices(ctx, objectsV1.GetServicesRequest{
Project: myProject.GetName(),
Names: []string{myService.GetName()},
})
if err != nil {
log.Fatalf("failed to get services, err: %v", err)
}
projects, err := client.Objects().V1().GetV1alphaProjects(ctx, objectsV1.GetProjectsRequest{
Names: []string{myProject.GetName()},
})
if err != nil {
log.Fatalf("failed to get projects, err: %v", err)
}

// Aggregate objects back into manifest.Objects slice.
appliedObjects := make([]manifest.Object, 0, len(services)+len(projects))
for _, service := range services {
appliedObjects = append(appliedObjects, service)
}
for _, project := range projects {
appliedObjects = append(appliedObjects, project)
}

// Print JSON representation of these objects.
data, err := json.MarshalIndent(appliedObjects, "", " ")
if err != nil {
log.Fatalf("failed to marshal objects, err: %v", err)
}
fmt.Println(string(data))

// Delete resources.
if err = client.Objects().V1().Delete(ctx, objects); err != nil {
log.Fatalf("failed to delete objects, err: %v", err)
}
ctx := context.Background()

// Create client.
client, err := sdk.DefaultClient()
if err != nil {
log.Fatalf("failed to create sdk client, err: %v", err)
}

// Read from file, url or glob pattern.
objects, err := sdk.ReadObjects(ctx, "./project.yaml")
if err != nil {
log.Fatalf("failed to read project.yaml file, err: %v", err)
}
// Use manifest.FilterByKind to extract specific objects from the manifest.Object slice.
myProject := manifest.FilterByKind[project.Project](objects)[0]
// Define objects in code.
myService := service.New(
service.Metadata{
Name: "my-service",
DisplayName: "My Service",
Project: myProject.GetName(),
Labels: v1alpha.Labels{
"team": []string{"green", "orange"},
"region": []string{"eu-central-1"},
},
},
service.Spec{
Description: "Example service",
},
)
objects = append(objects, myService)

// Verify the objects.
if errs := manifest.Validate(objects); len(errs) > 0 {
log.Fatalf("service validation failed, errors: %v", errs)
}

// Apply the objects.
if err = client.Objects().V1().Apply(ctx, objects); err != nil {
log.Fatalf("failed to apply objects, err: %v", err)
}

// Get the applied resources.
services, err := client.Objects().V1().GetV1alphaServices(ctx, objectsV1.GetServicesRequest{
Project: myProject.GetName(),
Names: []string{myService.GetName()},
})
if err != nil {
log.Fatalf("failed to get services, err: %v", err)
}
projects, err := client.Objects().V1().GetV1alphaProjects(ctx, objectsV1.GetProjectsRequest{
Names: []string{myProject.GetName()},
})
if err != nil {
log.Fatalf("failed to get projects, err: %v", err)
}

// Aggregate objects back into manifest.Objects slice.
appliedObjects := make([]manifest.Object, 0, len(services)+len(projects))
for _, service := range services {
appliedObjects = append(appliedObjects, service)
}
for _, project := range projects {
appliedObjects = append(appliedObjects, project)
}

// Print JSON representation of these objects.
data, err := json.MarshalIndent(appliedObjects, "", " ")
if err != nil {
log.Fatalf("failed to marshal objects, err: %v", err)
}
fmt.Println(string(data))

// Delete resources.
if err = client.Objects().V1().Delete(ctx, objects); err != nil {
log.Fatalf("failed to delete objects, err: %v", err)
}
}
```

<!-- markdownlint-enable MD013 -->

# Usage

## Reading configuration

In order for `sdk.Client` to work, it needs to be configured.
The configuration can be read from a file, environment variables,
code options or a combination of these.

The precedence of the configuration sources is as follows
(starting from the highest):

- Code options
- Environment variables
- Configuration file
- Default values

The following flowchart illustrates the process of reading the configuration:

```mermaid
flowchart TD
subgraph s1[Read config file]
direction LR
As1{{Config file exists}} -- true --> Bs1(Read config file)
As1 -- false --> Cs1(Create default config file)
Cs1 --> Bs1
end
subgraph s2[Build config struct]
direction LR
As2{{Has ConfigOption}} -- not set --> Bs2{{Has env variable}}
As2 -- set --> Fs2(Use value)
Bs2 -- not set --> Cs2{{Has config file option}}
Bs2 -- set --> Fs2
Cs2 -- not set --> Ds2{{Has default value}}
Cs2 -- set --> Fs2
Ds2 -- not set --> Es2(No value)
Ds2 -- set --> Fs2
end
A(Read config) --> B(Read config options defined in code)
B --> C(Read env variables)
C --> s1
s1 --> s2 --> I(Return Config)
```

## Testing code relying on nobl9-go

Checkout [these instructions](./docs/mock_example/README.md)
along with a working example for recommendations on mocking `sdk.Client`.

# Repository structure

## Public packages
Expand Down Expand Up @@ -184,6 +235,14 @@ func main() {
- Object-specific packages, like [slo](./manifest/v1alpha/slo), provide
object definition for specific object versions.

## Internal packages

1. [tests](./tests) contains the end-to-end tests code.
These tests are run directly against a Nobl9 platform.
2. [internal](./internal) holds internal packages that are not meant to be
exposed as part of the library's API.

# Contributing

TBA
Checkout both [contributing guidelines](./docs/CONTRIBUTING.md) and
[development instructions](./docs/DEVELOPMENT.md).
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ words:
- generify
- gobin
- gofile
- gofmt
- goimports
- golangci
- gomnd
Expand Down
31 changes: 31 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contributing to nobl9-go

If you're here, chances are you want to contribute ;)
Thanks a lot for that!

Your pull request will be reviewed by one of the maintainers.
We encourage and welcome any and all feedback.

## Before you contribute

The goal of this project is to provide a feature-rich and easy-to-use
Golang SDK for Nobl9 platform.

Make sure you're familiarized with
[development instructions](./DEVELOPMENT.md).

## Making a pull request

Please make a fork of this repo and submit a PR from there.
More information can be found
[here](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request).

## Merge Request title

Try to be as descriptive as you can in your PR title.
Note that the title must adhere to the rules defined in
[this workflow](./.github/workflows/pr-title.yml).

## License

Nobl9-go is licensed under Mozilla Public License Version 2.0, see [LICENSE](../LICENSE).
5 changes: 3 additions & 2 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ We use the following tools to do that:

## Validation

We're using our own validation library to write validation for all objects.
Refer to this [README.md](../internal/validation/README.md) for more information.
We're using [govy](https://github.com/nobl9/govy) library for validation.
If you encounter any bugs or shortcomings feel free to open an issue or PR
at govy's GitHub page.

## Dependencies

Expand Down
Loading

0 comments on commit 45a5c1c

Please sign in to comment.