userz

package module
v0.0.0-...-26b8432 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 5, 2022 License: GPL-3.0 Imports: 12 Imported by: 0

README

Userz

A simple demonstration project for a k8s microservice

The project aims to exemplify a very simple cloud native microservice to handle a store of users. The json schema is the following:

{
    "id": string,
    "first_name": optional<string>,
    "last_name": optional<string>,
    "nickname": string,
    "email": string,
    "password": bytes, // bcrypt of a given string
    "country": optional<string>,
    "created_at": optional<timestamp with timezone>,
    "updated_at": optional<timestamp with timezone>
}

The service exposes the expected CRUD interface both on HTTP and via gRPC, has a notification mechanism based on plugins and offers both and healthcheck endpoint and prometheus metrics.

Run the project

To interact with the project, one may use GNU make to spin up a docker compose stack. These, together with curl and git, are the only prerequisites.

make run

compiles the image and starts the project. To stop and clean everything

make stop

While running the project, some mock data can be added via

make data

The project has a somehow comprehensive test coverage. The unit tests can be run with

make test

The integration tests can be run with

make test-integration

In case of failure, the stack for the integration tests remains up, so that one can examine the database (exposed at localhost:5432). To clean the stack after a failed run (no need in case of a successful run)

make test-clean

Some details

The executable

The userz executable currently offers the following configurations:

NAME:
   userz - manage a list of users

USAGE:
   userz [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --debug                      Set logging to debug level (defaults to info) (default: false) [$DEBUG]
   --console                    Enable pretty (and slower) logging (default: false)
   --http-port value            The port on which the HTTP API will be exposed (default: 6000) [$HTTP_PORT]
   --grpc-port value            The port on which the gRPC API will be exposed (default: 7000) [$GRPC_PORT]
   --grpc-cert value            The path to a TLS certificate to use with the gRPC endpoint [$GRPC_CERT]
   --grpc-key value             The path to a TLS key to use with the gRPC endpoint [$GRPC_KEY]
   --metrics-port value         The port on which the metrics will be exposed (healthcheck and prometheus) (default: 25000) [$METRICS_PORT]
   --pgurl value                The url to connect to the postgres database (if specified, supercedes all other postgres flags) [$POSTGRES_URL]
   --pguser value               The user to connect to the postgres database [$POSTGRES_USER]
   --pghost value               The host to connect to the postgres database (default: "localhost") [$POSTGRES_HOST]
   --pgpassword value           The password to connect to the postgres database [$POSTGRES_PASSWORD]
   --pgport value               The port to connect to the postgres database (default: 5432) [$POSTGRES_PORT]
   --pgdbname value             The dbname to connect to the postgres database [$POSTGRES_DBNAME]
   --pgssl                      Whether to connect to the postgres database in strict ssl mode (default: false) [$POSTGRES_SSL]
   --disable-notifications      Whether to disable notifications (default: false) [$DISABLE_NOTIFICATIONS]
   --notification-plugin value  Specify path to the .so that provides the notification functionality (default: "/pollednotifier.so") [$NOTIFICATION_PLUGIN]
   --help, -h                   show help (default: false)
The HTTP REST API

The api is exposed, by default, at http://localhost:6000/api (the port is configurable). It follows the REST paradigm, so

  • Creation is a PUT at /api and returns the id of the newly created entity.
  • Update is a POST at /api/{id}, and returns the whole updated entity.
  • Remove is a DELETE at /api/{id}, and returns the whole deleted entity.
  • Access is a GET at /api, with an optional filter and a mandatory pageSize and offset parameters, expected to be positive integers.

Both the creation and the update expect a JSON body with the following schema

{
    "first_name": optional<string>,
    "last_name": optional<string>,
    "nickname": string,
    "email": string,
    "password": string, // the plaintext, will be stored bcrypt'ed
    "country": optional<string>,
}
The gRPC API

The gRPC API follows along the lines of the HTTP one, except for the access: it is a stream that must be consumed linearly. The protobuf definition is at pkg/proto/userz.proto. It is importable externally using

github.com/leophys/userz/pkg/proto
The notification system

Notifications follow an extensible mechanism, based on the stdlib plugin module. The default implementation, which can be used as model for future implementations to support other technologies, lies at internal/pollednotifier. It exposes a /notifications HTTP endpoint (by default on port 8000) and every GET towards that endpoint returns a JSON array that gets consumed at every access.

The public interface that has to be implemented by other plugins is at pkg/notifier/notifier.go

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoMorePages = errors.New("the iterator has been consumed")

Functions

func Hash

func Hash(str string) string

func ParseConditionable

func ParseConditionable[T Conditionable](v string) (any, error)

ParseConditionable tries to transform a value in a string condition into the specified type T. The following mapping from strings are supported:

  • string -> string
  • int -> int
  • time.RFC3339 -> time.Time

func ValidateOp

func ValidateOp[T Conditionable](op Op, value T, values ...T) error

Types

type Cond

type Cond[T Conditionable] struct {
	Op
	Value  T
	Values []T
}

func ParseCondition

func ParseCondition[T Conditionable](c string) (cond Cond[T], err error)

ParseCondition tries to parse a string into a condition, matching on it and trying to cast the match into the proper Cond[T].

func (Cond[T]) Evaluate

func (Cond[T]) Evaluate(string) (any, error)

func (Cond[T]) Hash

func (Cond[T]) Hash(string) (string, error)

type Condition

type Condition[T Conditionable] interface {
	// Evaluate translates the abstract Condition into a form that is
	// usable by the backend.
	Evaluate(field string) (any, error)
	// Hash returns a unique identified deterministically derived by the
	// values of the condition.
	Hash(field string) (string, error)
}

Condition is the interface any backend will need to implement in order to translate the given condition in a valid expression for the backend at hand.

type Conditionable

type Conditionable interface {
	constraints.Integer | constraints.Float | string | time.Time
}

type Filter

type Filter struct {
	Id        string
	FirstName Condition[string]
	LastName  Condition[string]
	NickName  Condition[string]
	Email     Condition[string]
	Country   Condition[string]
	CreatedAt Condition[time.Time]
	UpdatedAt Condition[time.Time]
}

Filter is a condition to be used to filter users. The backend type represents the output type a concrete implementation will produce as output of the evaluation of the filter.

func ParseFilter

func ParseFilter(inputMap map[string]string) (*Filter, error)

ParseFilter takes an optional map with a set of conditions to be parsed and, upon successful parsing of each condition, returns a *Filter.

func (*Filter) Hash

func (f *Filter) Hash() (string, error)

Hash returns a unique identifier of the filter.

type Iterator

type Iterator[T any] interface {
	// Len returns some data regarding the pagination.
	Len() PaginationData
	// Next returns the next page. It returns ErrNoMorePages after the last page.
	Next(ctx context.Context) (T, error)
}

Iterator is the interface to iterate over the results.

type Op

type Op int
const (
	OpEq Op = iota
	OpNe
	OpGt
	OpGe
	OpLt
	OpLe
	OpInside
	OpOutside
	OpBegins
	OpEnds
)

func (Op) String

func (o Op) String() string

type OrdBy

type OrdBy string
const (
	OrdByFirstName OrdBy = "first_name"
	OrdByLastName  OrdBy = "last_name"
	OrdByNickName  OrdBy = "nick_name"
	OrdByEmail     OrdBy = "email"
	OrdByCreatedAt OrdBy = "created_at"
	OrdByUpdatedAt OrdBy = "updated_at"
)

func (OrdBy) String

func (o OrdBy) String() string

type OrdDir

type OrdDir bool
const (
	OrdDirDesc OrdDir = true
	OrdDirAsc  OrdDir = false
)

func (OrdDir) String

func (o OrdDir) String() string

type Order

type Order struct {
	OrdBy
	OrdDir
}

func ParseOrder

func ParseOrder(ordBy, ordDir string) (Order, error)

func (Order) String

func (o Order) String() string

type PageParams

type PageParams struct {
	Size   uint
	Offset uint
	Order  Order
}

PageParams conveys the information needed to specify a page for the Page method.

type PaginationData

type PaginationData struct {
	TotalElements uint
	TotalPages    uint
	PageSize      uint
}

PaginationData regards the global information pertaining the pagination.

type Password

type Password []byte

Password represents a secret to be stored safely at rest.

func NewPassword

func NewPassword(plaintext string) (Password, error)

func (Password) String

func (p Password) String() string

type Passworder

type Passworder func(string) (Password, error)

type ReprCondition

type ReprCondition[T Conditionable] Cond[T]

func (*ReprCondition[T]) Evaluate

func (c *ReprCondition[T]) Evaluate(field string) (result any, err error)

func (*ReprCondition[T]) Hash

func (c *ReprCondition[T]) Hash(field string) (string, error)

type Store

type Store interface {
	Add(ctx context.Context, user *UserData) (*User, error)
	Update(ctx context.Context, id string, user *UserData) (*User, error)
	Remove(ctx context.Context, id string) (*User, error)
	List(ctx context.Context, filter *Filter, pageSize uint) (Iterator[[]*User], error)
	Page(ctx context.Context, filter *Filter, params *PageParams) ([]*User, error)
}

Store represents the storage backend for the Users.

type User

type User struct {
	Id        string    `json:"id"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	NickName  string    `json:"nickname"`
	Password  Password  `json:"-"`
	Email     string    `json:"email"`
	Country   string    `json:"country"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

User is the main entity we handle, it contains all the needed information

type UserData

type UserData struct {
	FirstName string `json:"first_name,omitempty"`
	LastName  string `json:"last_name,omitempty"`
	NickName  string `json:"nickname,omitempty"`
	Password  string `json:"password,omitempty"`
	Email     string `json:"email,omitempty"`
	Country   string `json:"country,omitempty"`
}

UserData represents the data needed to create or alter a user.

Directories

Path Synopsis
cmd
pkg
notifier
The notifier package
The notifier package
proto
This package holds the gRPC definitions and generated code
This package holds the gRPC definitions and generated code
store
pg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL