├── .dockerignore
├── .gitignore
├── pkg
├── control
│ ├── migrations
│ │ ├── 000005_create_accounts_table.down.sql
│ │ ├── 000003_create_label_links_table.down.sql
│ │ ├── 000009_add_account_data.down.sql
│ │ ├── 000007_create_activity_log_table.down.sql
│ │ ├── 000009_add_account_data.up.sql
│ │ ├── 000004_create_management_clients_table.down.sql
│ │ ├── 000008_create_hub_table.down.sql
│ │ ├── 000002_create_services_table.down.sql
│ │ ├── 000006_create_jobs_table.down.sql
│ │ ├── 000004_create_management_clients_table.up.sql
│ │ ├── 000007_create_activity_log_table.up.sql
│ │ ├── 000005_create_accounts_table.up.sql
│ │ ├── 000003_create_label_links_table.up.sql
│ │ ├── 000008_create_hub_table.up.sql
│ │ ├── 000002_create_services_table.up.sql
│ │ └── 000006_create_jobs_table.up.sql
│ ├── activity_bg.go
│ ├── zstd.go
│ ├── server_http_test.go
│ ├── labels.go
│ ├── config.go
│ ├── client_k8s.go
│ ├── activity_test.go
│ ├── server_edge.go
│ ├── flow_top.go
│ ├── client_edge.go
│ ├── server_http.go
│ ├── lockmgr.go
│ └── activity.go
├── pb
│ ├── gen.go
│ ├── timestamp.proto
│ ├── kv.proto
│ ├── network.proto
│ ├── label.proto
│ ├── ulid.proto
│ ├── account.proto
│ ├── kv.go
│ ├── timestamp.go
│ ├── kv.pb.json.go
│ ├── timestamp.pb.json.go
│ ├── network.pb.json.go
│ ├── network.go
│ ├── edge.proto
│ ├── label.pb.json.go
│ ├── ulid.pb.json.go
│ ├── account.pb.json.go
│ ├── token.proto
│ ├── flow.proto
│ ├── ulid.go
│ ├── account.go
│ ├── label_test.go
│ ├── wire.proto
│ ├── edge.pb.json.go
│ ├── token.pb.json.go
│ └── flow.pb.json.go
├── labels
│ ├── compress.go
│ └── parse.go
├── token
│ ├── labels.go
│ ├── armor.go
│ ├── metadata.go
│ ├── vault_test.go
│ ├── token.go
│ ├── vault.go
│ ├── validate.go
│ ├── creator.go
│ └── token_test.go
├── testutils
│ ├── vault.go
│ ├── cert.go
│ └── s3.go
├── wire
│ ├── wire.go
│ ├── bufconn.go
│ ├── marshal_bytes.go
│ ├── framing_test.go
│ ├── ulid.go
│ ├── adapt.go
│ ├── rpc.go
│ ├── context.go
│ └── framing.go
├── hub
│ ├── hub_http.go
│ ├── stats.go
│ └── consul_health_test.go
├── periodic
│ └── run.go
├── dbx
│ └── gorm.go
├── agent
│ ├── label.go
│ ├── echo.go
│ ├── query.go
│ ├── tcp.go
│ ├── http.go
│ ├── connect.go
│ └── agent_test.go
├── data
│ ├── memory.go
│ └── config.go
├── x
│ └── debug_reader.go
├── grpc
│ ├── token
│ │ └── token.go
│ └── lz4
│ │ └── lz4.go
├── utils
│ ├── vault.go
│ └── cert.go
├── workq
│ ├── job.go
│ ├── default.go
│ ├── registry_test.go
│ ├── periodic.go
│ ├── injector.go
│ ├── periodic_test.go
│ └── registry.go
├── tlsmanage
│ ├── renew_bg.go
│ └── vault.go
├── web
│ ├── tls.go
│ └── test
│ │ └── web_test.go
├── discovery
│ ├── hub.go
│ └── server.go
├── config
│ └── db.go
├── timing
│ └── timing.go
├── netloc
│ ├── best_test.go
│ └── best.go
└── connect
│ └── connect.go
├── .envrc
├── .github
└── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
├── kubernetes
├── hub-service.yml
├── control-service.yml
├── control-ingress.yml
└── hub.yml
├── internal
├── httpassets
│ ├── static
│ │ ├── images
│ │ │ ├── logo.svg
│ │ │ ├── error.svg
│ │ │ └── hashi.svg
│ │ ├── error_limit.html
│ │ ├── error.html
│ │ └── index.html
│ └── httpassets.go
└── sqljson
│ ├── data.go
│ └── data_test.go
├── cmd
├── netloc
│ └── main.go
├── ulidgen
│ └── main.go
└── hznagent
│ └── pipe.go
├── labels.go
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── shell.nix
├── README.md
└── Makefile
/.dockerignore:
--------------------------------------------------------------------------------
1 | bin/
2 | tmp/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | tmp/
3 | dev-*.txt
4 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000005_create_accounts_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS accounts;
5 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000003_create_label_links_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS label_links;
5 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000009_add_account_data.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | ALTER TABLE accounts DROP COLUMN data;
5 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | # If we are a computer with nix-shell available, then use that to setup
2 | # the build environment with exactly what we need.
3 | if has nix-shell; then
4 | use nix
5 | fi
6 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000007_create_activity_log_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS activity_logs;
5 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000009_add_account_data.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | ALTER TABLE accounts ADD COLUMN data bytea NULL;
5 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000004_create_management_clients_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS managament_clients;
5 |
--------------------------------------------------------------------------------
/pkg/pb/gen.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | //go:generate sh -c "protoc --go-json_out=. --gogoslick_out=plugins=grpc:. *.proto"
5 | package pb
6 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000008_create_hub_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP INDEX IF EXISTS hub_instance_id;
5 | DROP TABLE IF EXISTS hubs;
6 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000002_create_services_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS services;
5 | DROP INDEX IF EXISTS account_services;
6 |
--------------------------------------------------------------------------------
/pkg/pb/timestamp.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | package pb;
7 |
8 | message Timestamp {
9 | uint64 sec = 1;
10 | uint64 nsec = 2;
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000006_create_jobs_table.down.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | DROP TABLE IF EXISTS periodic_jobs;
5 | DROP TABLE IF EXISTS jobs;
6 | DROP TYPE IF EXISTS job_status;
7 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000004_create_management_clients_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS management_clients (
5 | id bytea PRIMARY KEY,
6 | namespace text NOT NULL
7 | );
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | contact_links:
5 | - name: Ask a question
6 | url: https://discuss.hashicorp.com/c/waypoint
7 | about: For increased visibility, please post questions on the discussion forum.
8 |
--------------------------------------------------------------------------------
/pkg/pb/kv.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | package pb;
7 |
8 | message KVPair {
9 | string key = 1;
10 | int64 ikey = 2;
11 |
12 | string value = 3;
13 | int64 ivalue = 4;
14 | bytes bvalue = 5;
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/labels/compress.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package labels
5 |
6 | import (
7 | "sort"
8 | "strings"
9 | )
10 |
11 | func CompressLabels(v []string) string {
12 | sort.Strings(v)
13 |
14 | return strings.ToLower(strings.Join(v, ", "))
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/pb/network.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "label.proto";
7 |
8 | package pb;
9 |
10 | message NetworkLocation {
11 | repeated string addresses = 1;
12 | LabelSet labels = 2;
13 | string name = 3;
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000007_create_activity_log_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS activity_logs (
5 | id bigserial PRIMARY KEY,
6 | event jsonb NOT NULL,
7 | created_at timestamp(6) with time zone NOT NULL DEFAULT now()
8 | );
9 |
--------------------------------------------------------------------------------
/pkg/labels/parse.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package labels
5 |
6 | import "strings"
7 |
8 | func ParseLabel(s string) (string, string) {
9 | idx := strings.IndexByte(s, '=')
10 | if idx == -1 {
11 | return s, ""
12 | }
13 |
14 | return s[:idx], s[idx+1:]
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/token/labels.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | const (
7 | KeyAlgo = 1
8 | KeyAccountId = 100
9 | KeyAccountNamespace = 101
10 | KeyCapabilities = 110
11 | )
12 |
13 | const (
14 | ValueHMAC = 1
15 | ValueED25519 = 2
16 | )
17 |
--------------------------------------------------------------------------------
/kubernetes/hub-service.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | apiVersion: v1
5 | kind: Service
6 | metadata:
7 | name: hub
8 | spec:
9 | selector:
10 | app: hub
11 | ports:
12 | - protocol: TCP
13 | port: 443
14 | targetPort: 443
15 | nodePort: 443
16 | type: NodePort
17 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000005_create_accounts_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS accounts (
5 | id bytea PRIMARY KEY,
6 | namespace text NOT NULL,
7 |
8 | created_at timestamp NOT NULL DEFAULT now(),
9 | updated_at timestamp NOT NULL DEFAULT now()
10 | );
11 |
--------------------------------------------------------------------------------
/pkg/token/armor.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "github.com/mr-tron/base58"
8 | )
9 |
10 | func Armor(token []byte) string {
11 | return base58.Encode(token)
12 | }
13 |
14 | func RemoveArmor(token string) ([]byte, error) {
15 | return base58.Decode(token)
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/pb/label.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "defs/gogo.proto";
7 |
8 | package pb;
9 |
10 | message Label {
11 | string name = 1;
12 | string value = 2;
13 | }
14 |
15 | message LabelSet {
16 | option (gogoproto.stringer) = false;
17 | repeated Label labels = 1;
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/testutils/vault.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package testutils
5 |
6 | import (
7 | "github.com/hashicorp/horizon/pkg/utils"
8 | "github.com/hashicorp/vault/api"
9 | )
10 |
11 | const DefaultTestRootId = utils.DefaultTestRootId
12 |
13 | func SetupVault() *api.Client {
14 | return utils.SetupVault()
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/wire/wire.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "time"
8 |
9 | "github.com/hashicorp/horizon/pkg/pb"
10 | )
11 |
12 | func Now() *pb.Timestamp {
13 | t := time.Now()
14 |
15 | return &pb.Timestamp{
16 | Sec: uint64(t.Unix()),
17 | Nsec: uint64(t.Nanosecond()),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/hub/hub_http.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package hub
5 |
6 | import "net/http"
7 |
8 | func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9 | h.mux.ServeHTTP(w, r)
10 | }
11 |
12 | func (h *Hub) handleHeathz(w http.ResponseWriter, r *http.Request) {
13 | w.WriteHeader(200)
14 | w.Write([]byte("ok"))
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/pb/ulid.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "defs/gogo.proto";
7 |
8 | package pb;
9 |
10 | message ULID {
11 | option (gogoproto.stringer) = false;
12 | uint64 timestamp = 1;
13 | bytes entropy = 2;
14 | }
15 |
16 | message ULIDWithDuration {
17 | ULID ulid = 1;
18 | uint64 elapse = 2;
19 | }
20 |
--------------------------------------------------------------------------------
/kubernetes/control-service.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | apiVersion: v1
5 | kind: Service
6 | metadata:
7 | annotations:
8 | service.beta.kubernetes.io/aws-load-balancer-type: nlb
9 | name: control
10 | spec:
11 | selector:
12 | app: control
13 | ports:
14 | - protocol: TCP
15 | port: 443
16 | targetPort: 443
17 | type: LoadBalancer
18 |
--------------------------------------------------------------------------------
/pkg/wire/bufconn.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "bufio"
8 | "io"
9 | )
10 |
11 | type ComposedConn struct {
12 | *bufio.Reader
13 | io.Writer
14 | io.Closer
15 | Recyclable
16 | }
17 |
18 | func (c *ComposedConn) BufioReader() *bufio.Reader {
19 | return c.Reader
20 | }
21 |
22 | // var _ yamux.BufioReaderer = &ComposedConn{}
23 |
--------------------------------------------------------------------------------
/pkg/testutils/cert.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package testutils
5 |
6 | import (
7 | "crypto/tls"
8 |
9 | "github.com/hashicorp/horizon/pkg/utils"
10 | )
11 |
12 | func SelfSignedCert() ([]byte, []byte, error) {
13 | return utils.SelfSignedCert()
14 | }
15 |
16 | func TrustedTLSConfig(cert []byte) (*tls.Config, error) {
17 | return utils.TrustedTLSConfig(cert)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/periodic/run.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package periodic
5 |
6 | import (
7 | "context"
8 | "time"
9 | )
10 |
11 | func Run(ctx context.Context, period time.Duration, f func()) {
12 | ticker := time.NewTicker(period)
13 | defer ticker.Stop()
14 |
15 | for {
16 | select {
17 | case <-ctx.Done():
18 | return
19 | case <-ticker.C:
20 | f()
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000003_create_label_links_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS label_links (
5 | id serial PRIMARY KEY,
6 | account_id bytea NOT NULL,
7 | labels text NOT NULL,
8 | target text NOT NULL,
9 |
10 | created_at timestamp NOT NULL DEFAULT now(),
11 | updated_at timestamp NOT NULL DEFAULT now(),
12 |
13 | UNIQUE(account_id, labels)
14 | )
15 |
--------------------------------------------------------------------------------
/internal/httpassets/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000008_create_hub_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS hubs (
5 | stable_id bytea PRIMARY KEY,
6 | instance_id bytea UNIQUE,
7 | connection_info jsonb,
8 | last_checkin timestamp with time zone NOT NULL,
9 | created_at timestamp with time zone NOT NULL DEFAULT now()
10 | );
11 |
12 | CREATE INDEX IF NOT EXISTS hub_instance_id ON hubs (instance_id);
13 |
--------------------------------------------------------------------------------
/pkg/dbx/gorm.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package dbx
5 |
6 | import "github.com/hashicorp/go-multierror"
7 |
8 | type HasErrors interface {
9 | GetErrors() []error
10 | }
11 |
12 | func Check(x HasErrors) error {
13 | errs := x.GetErrors()
14 | switch len(errs) {
15 | case 0:
16 | return nil
17 | case 1:
18 | return errs[0]
19 | default:
20 | return multierror.Append(nil, errs...)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/pb/account.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "ulid.proto";
7 | import "defs/gogo.proto";
8 |
9 | package pb;
10 |
11 | message Account {
12 | option (gogoproto.stringer) = false;
13 | string namespace = 1;
14 | ULID account_id = 2;
15 |
16 | message Limits {
17 | double http_requests = 1; // per second
18 | double bandwidth = 2; // in KB/s
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/pkg/pb/kv.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | import (
7 | "encoding/base64"
8 | "strconv"
9 | )
10 |
11 | func (kv *KVPair) ValueString() string {
12 | switch {
13 | case kv.Value != "":
14 | return kv.Value
15 | case kv.Bvalue != nil:
16 | return base64.StdEncoding.EncodeToString(kv.Bvalue)
17 | case kv.Ivalue != 0:
18 | return strconv.FormatInt(kv.Ivalue, 10)
19 | default:
20 | return ""
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/internal/httpassets/httpassets.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | // Package httpassets contain the assets for HTML pages that Horizon
5 | // serves. This is temporary since this is Waypoint-specific and Horizon
6 | // is not Waypoint-specific. But we needed this done before launch so here we
7 | // are making technical debt for ourselves.
8 | package httpassets
9 |
10 | //go:generate go-bindata -pkg httpassets -fs -prefix "static/" static/...
11 |
--------------------------------------------------------------------------------
/pkg/agent/label.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import "strings"
7 |
8 | type Label struct {
9 | Name, Value string
10 | }
11 |
12 | func (l *Label) String() string {
13 | return l.Name + "=" + l.Value
14 | }
15 |
16 | func ParseLabel(s string) Label {
17 | idx := strings.IndexByte(s, '=')
18 | if idx == -1 {
19 | return Label{Name: s}
20 | }
21 |
22 | return Label{Name: s[:idx], Value: s[idx+1:]}
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/wire/marshal_bytes.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | type MarshalBytes []byte
7 |
8 | func (m *MarshalBytes) Size() int {
9 | return len(*m)
10 | }
11 |
12 | func (m *MarshalBytes) MarshalTo(b []byte) (int, error) {
13 | return copy(b, *m), nil
14 | }
15 |
16 | func (m *MarshalBytes) Unmarshal(b []byte) error {
17 | data := make([]byte, len(b))
18 | copy(data, b)
19 | *m = data
20 |
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/data/memory.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package data
5 |
6 | import (
7 | "sync"
8 | )
9 |
10 | type Memory struct {
11 | mu sync.RWMutex
12 | data map[string]interface{}
13 | }
14 |
15 | func (m *Memory) Set(path string, data interface{}) error {
16 | m.mu.Lock()
17 | defer m.mu.Unlock()
18 |
19 | m.data[path] = data
20 |
21 | return nil
22 | }
23 |
24 | func (m *Memory) Get(path string) (interface{}, error) {
25 | return m.data[path], nil
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/netloc/main.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package main
5 |
6 | import (
7 | "encoding/json"
8 | "fmt"
9 | "log"
10 | "os"
11 |
12 | "github.com/hashicorp/horizon/pkg/netloc"
13 | )
14 |
15 | func main() {
16 | fmt.Fprintf(os.Stderr, "Gathering locations...\n")
17 | locs, err := netloc.Locate(nil)
18 | if err != nil {
19 | log.Fatal(err)
20 | }
21 |
22 | enc := json.NewEncoder(os.Stdout)
23 | enc.SetIndent("", " ")
24 |
25 | enc.Encode(locs)
26 | }
27 |
--------------------------------------------------------------------------------
/labels.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package horizon
5 |
6 | import "github.com/hashicorp/horizon/pkg/agent"
7 |
8 | func CombineLabels(a, b []agent.Label) []agent.Label {
9 | out := append([]agent.Label(nil), a...)
10 |
11 | outer:
12 | for _, outer := range b {
13 | for _, inner := range a {
14 | if outer.Name == inner.Name && outer.Value == inner.Value {
15 | continue outer
16 | }
17 | }
18 |
19 | out = append(out, outer)
20 | }
21 |
22 | return out
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/control/activity_bg.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 |
9 | "github.com/hashicorp/horizon/pkg/dbx"
10 | "github.com/jinzhu/gorm"
11 | )
12 |
13 | var LogPruneInterval = "6 hours"
14 |
15 | type LogCleaner struct {
16 | DB *gorm.DB
17 | }
18 |
19 | func (l *LogCleaner) CleanupActivityLog(ctx context.Context, jobType string, _ *struct{}) error {
20 | return dbx.Check(
21 | l.DB.Exec("DELETE FROM activity_logs WHERE created_at < now() - ?::interval", LogPruneInterval),
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/hub/stats.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package hub
5 |
6 | import (
7 | "sync/atomic"
8 |
9 | "github.com/hashicorp/horizon/pkg/pb"
10 | )
11 |
12 | func (h *Hub) sendAgentInfoFlow(ai *agentConn) {
13 | var rec pb.FlowRecord
14 |
15 | rec.Agent = &pb.FlowRecord_AgentConnection{
16 | HubId: h.id,
17 | AgentId: ai.ID,
18 | Account: ai.Account,
19 | StartedAt: ai.Start,
20 | EndedAt: ai.End,
21 | NumServices: ai.Services,
22 | ActiveStreams: atomic.LoadInt64(ai.ActiveStreams),
23 | }
24 |
25 | h.cc.SendFlow(&rec)
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/ulidgen/main.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package main
5 |
6 | import (
7 | "bytes"
8 | "encoding/hex"
9 | "fmt"
10 | "os"
11 |
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | )
14 |
15 | func main() {
16 | if len(os.Args) > 1 {
17 | rep := os.Args[1]
18 |
19 | data, err := hex.DecodeString(rep)
20 | if err != nil {
21 | panic(err)
22 | }
23 |
24 | idx := bytes.IndexByte(data, '!')
25 | if idx != -1 {
26 | data = data[idx+1:]
27 | }
28 |
29 | fmt.Println(pb.ULIDFromBytes(data).String())
30 | return
31 | }
32 |
33 | fmt.Println(pb.NewULID().String())
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/x/debug_reader.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package x
5 |
6 | import (
7 | "io"
8 |
9 | "github.com/davecgh/go-spew/spew"
10 | )
11 |
12 | type DebugReader struct {
13 | R io.Reader
14 | }
15 |
16 | func (d DebugReader) Read(b []byte) (int, error) {
17 | n, err := d.R.Read(b)
18 | spew.Printf("read: %v, %v, %v\n", err, n, b[:n])
19 | return n, err
20 | }
21 |
22 | type DebugWriter struct {
23 | W io.Writer
24 | }
25 |
26 | func (d DebugWriter) Write(b []byte) (int, error) {
27 | n, err := d.W.Write(b)
28 | spew.Printf("write: %v, %v, %+v\n", err, n, b)
29 | return n, err
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000002_create_services_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TABLE IF NOT EXISTS services (
5 | id serial PRIMARY KEY,
6 | service_id bytea NOT NULL,
7 | hub_id bytea NOT NULL,
8 | account_id bytea NOT NULL,
9 | type text NOT NULL,
10 | description text NOT NULL,
11 | labels text[] NOT NULL,
12 | created_at timestamp NOT NULL DEFAULT now(),
13 | updated_at timestamp NOT NULL DEFAULT now()
14 | );
15 |
16 | CREATE INDEX account_services ON services USING btree (account_id, id);
17 | CREATE INDEX service_by_service_id ON services USING btree (service_id);
18 |
--------------------------------------------------------------------------------
/internal/httpassets/static/images/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/pkg/pb/timestamp.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | import "time"
7 |
8 | func (t *Timestamp) Time() time.Time {
9 | return time.Unix(int64(t.Sec), int64(t.Nsec))
10 | }
11 |
12 | func NewTimestamp(t time.Time) *Timestamp {
13 | return &Timestamp{Sec: uint64(t.Unix()), Nsec: uint64(t.Nanosecond())}
14 | }
15 |
16 | func (t *Timestamp) ToDuration() time.Duration {
17 | return (time.Second * time.Duration(t.Sec)) + time.Duration(t.Nsec)
18 | }
19 |
20 | func TimestampFromDuration(dur time.Duration) *Timestamp {
21 | return &Timestamp{
22 | Sec: uint64(dur / time.Second),
23 | Nsec: uint64(dur % time.Second),
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/token/metadata.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import "github.com/hashicorp/horizon/pkg/pb"
7 |
8 | func Metadata(stoken string) (map[string]string, error) {
9 | md := map[string]string{}
10 |
11 | token, err := RemoveArmor(stoken)
12 | if err != nil {
13 | return nil, err
14 | }
15 |
16 | if token[0] != Magic {
17 | return nil, err
18 | }
19 |
20 | var t pb.Token
21 |
22 | err = t.Unmarshal(token[1:])
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | for _, h := range t.Metadata.Headers {
28 | if h.Key != "" {
29 | md[h.Key] = h.ValueString()
30 | }
31 | }
32 |
33 | return md, nil
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/grpc/token/token.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "context"
8 |
9 | "google.golang.org/grpc/credentials"
10 | )
11 |
12 | // Token implements the credentials provider interface to provide the
13 | // given string token in the way that Horizon expects authentication.
14 | type Token string
15 |
16 | func (t Token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
17 | return map[string]string{
18 | "authorization": string(t),
19 | }, nil
20 | }
21 |
22 | func (t Token) RequireTransportSecurity() bool {
23 | return false
24 | }
25 |
26 | var _ credentials.PerRPCCredentials = Token("")
27 |
--------------------------------------------------------------------------------
/pkg/agent/echo.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "context"
8 | "io"
9 |
10 | "github.com/hashicorp/go-hclog"
11 | "github.com/hashicorp/horizon/pkg/wire"
12 | )
13 |
14 | type echoHandler struct{}
15 |
16 | func (_ *echoHandler) HandleRequest(ctx context.Context, L hclog.Logger, sctx ServiceContext) error {
17 | var mb wire.MarshalBytes
18 |
19 | for {
20 | tag, err := sctx.ReadMarshal(&mb)
21 | if err != nil {
22 | if err == io.EOF {
23 | return nil
24 | }
25 |
26 | return err
27 | }
28 |
29 | sctx.WriteMarshal(tag, &mb)
30 | }
31 | }
32 |
33 | func EchoHandler() ServiceHandler {
34 | return &echoHandler{}
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/pb/kv.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: kv.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *KVPair) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *KVPair) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
--------------------------------------------------------------------------------
/internal/httpassets/static/images/hashi.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/kubernetes/control-ingress.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | apiVersion: extensions/v1beta1
5 | kind: Ingress
6 | metadata:
7 | annotations:
8 | kubernetes.io/ingress.class: alb
9 | alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:074735953993:certificate/56d500a2-a8b5-4734-b73b-6d246eab9afe
10 | alb.ingress.kubernetes.io/scheme: internet-facing
11 | alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
12 |
13 | name: control
14 | spec:
15 | rules:
16 | - host: control.alpha.hzn.network
17 | http:
18 | paths:
19 | - path: /+
20 | backend:
21 | serviceName: control
22 | servicePort: 80
23 |
24 |
--------------------------------------------------------------------------------
/pkg/pb/timestamp.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: timestamp.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *Timestamp) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *Timestamp) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/data/config.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package data
5 |
6 | import "go.etcd.io/bbolt"
7 |
8 | func (b *Bolt) GetConfig(key string) ([]byte, error) {
9 | var data []byte
10 | err := b.db.View(func(tx *bbolt.Tx) error {
11 | buk := tx.Bucket([]byte("config"))
12 | if buk == nil {
13 | return nil
14 | }
15 |
16 | data = buk.Get([]byte(key))
17 | return nil
18 | })
19 |
20 | return data, err
21 | }
22 |
23 | func (b *Bolt) SetConfig(key string, val []byte) error {
24 | return b.db.Update(func(tx *bbolt.Tx) error {
25 | buk, err := tx.CreateBucketIfNotExists([]byte("config"))
26 | if err != nil {
27 | return err
28 | }
29 |
30 | return buk.Put([]byte(key), val)
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/pb/network.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: network.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *NetworkLocation) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *NetworkLocation) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/utils/vault.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package utils
5 |
6 | import (
7 | "os"
8 |
9 | "github.com/hashicorp/vault/api"
10 | )
11 |
12 | const DefaultTestRootId = "hznroot"
13 |
14 | func SetupVault() *api.Client {
15 | vt := os.Getenv("VAULT_TOKEN")
16 | if vt == "" {
17 | vt = DefaultTestRootId
18 | }
19 |
20 | var cfg api.Config
21 | cfg.Address = "http://127.0.0.1:8200"
22 | vc, err := api.NewClient(&cfg)
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | vc.SetToken(vt)
28 |
29 | vc.Sys().Mount("transit", &api.MountInput{
30 | Type: "transit",
31 | })
32 |
33 | vc.Sys().Mount("kv", &api.MountInput{
34 | Type: "kv",
35 | Options: map[string]string{
36 | "version": "2",
37 | },
38 | })
39 | return vc
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/workq/job.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "encoding/json"
8 | "time"
9 |
10 | "github.com/hashicorp/horizon/pkg/pb"
11 | )
12 |
13 | type Job struct {
14 | Id []byte `gorm:"primary_key"`
15 | Queue string
16 | Status string
17 | JobType string
18 | Payload []byte
19 |
20 | CoolOffUntil *time.Time
21 | Attempts int
22 |
23 | CreatedAt time.Time
24 | }
25 |
26 | func (j *Job) Set(jt string, v interface{}) error {
27 | j.JobType = jt
28 |
29 | data, err := json.Marshal(v)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | j.Payload = data
35 | return nil
36 | }
37 |
38 | func NewJob() *Job {
39 | var j Job
40 | j.Id = pb.NewULID().Bytes()
41 | j.Status = "queued"
42 |
43 | return &j
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/pb/network.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | func (l *NetworkLocation) SameLabels(r *NetworkLocation) bool {
7 | if l.Labels == nil || r.Labels == nil {
8 | return false
9 | }
10 |
11 | return l.Labels.Equal(r.Labels)
12 | }
13 |
14 | func (l *NetworkLocation) Cardinality(r *NetworkLocation) int {
15 | if l.Labels == nil || r.Labels == nil {
16 | return 0
17 | }
18 |
19 | var card int
20 |
21 | for _, llbl := range l.Labels.Labels {
22 | for _, rlbl := range r.Labels.Labels {
23 | if llbl.Equal(rlbl) {
24 | card++
25 | }
26 | }
27 | }
28 |
29 | return card
30 | }
31 |
32 | func (l *NetworkLocation) IsPublic() bool {
33 | for _, lbl := range l.Labels.Labels {
34 | if lbl.Name == "type" {
35 | return lbl.Value == "public"
36 | }
37 | }
38 |
39 | return false
40 | }
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest something!
4 | title: ''
5 | labels: new
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Explain any additional use-cases**
20 | If there are any use-cases that would help us understand the use/need/value please share them as they can help us decide on acceptance and prioritization.
21 |
22 | **Additional context**
23 | Add any other context or screenshots about the feature request here.
24 |
--------------------------------------------------------------------------------
/pkg/pb/edge.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "label.proto";
7 | import "account.proto";
8 | import "control.proto";
9 |
10 | package pb;
11 |
12 | message LookupEndpointsRequest {
13 | Account account = 1;
14 | LabelSet labels = 2;
15 | }
16 |
17 | message LookupEndpointsResponse {
18 | repeated ServiceRoute routes = 1;
19 | int64 cache_time = 2;
20 | }
21 |
22 | message ResolveLabelLinkRequest {
23 | LabelSet labels = 1;
24 | }
25 |
26 | message ResolveLabelLinkResponse {
27 | Account account = 1;
28 | LabelSet labels = 2;
29 | Account.Limits limits = 3;
30 | int64 cache_time = 4;
31 | }
32 |
33 | service EdgeServices {
34 | rpc LookupEndpoints(LookupEndpointsRequest) returns (LookupEndpointsResponse) {}
35 | rpc ResolveLabelLink(ResolveLabelLinkRequest) returns (ResolveLabelLinkResponse) {}
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/control/migrations/000006_create_jobs_table.up.sql:
--------------------------------------------------------------------------------
1 | -- Copyright (c) HashiCorp, Inc.
2 | -- SPDX-License-Identifier: MPL-2.0
3 |
4 | CREATE TYPE job_status AS ENUM ('queued', 'finished');
5 |
6 | CREATE TABLE IF NOT EXISTS jobs (
7 | id bytea PRIMARY KEY,
8 | queue text NOT NULL,
9 | status job_status NOT NULL DEFAULT 'queued',
10 | job_type text NOT NULL,
11 | payload jsonb,
12 | cool_off_until timestamp with time zone,
13 | attempts int NOT NULL DEFAULT 0,
14 | created_at timestamp with time zone NOT NULL DEFAULT now()
15 | );
16 |
17 | CREATE TABLE IF NOT EXISTS periodic_jobs (
18 | id serial PRIMARY KEY,
19 | name text NOT NULL UNIQUE,
20 | queue text NOT NULL,
21 | job_type text NOT NULL,
22 | payload jsonb,
23 | period text NOT NULL DEFAULT '1h',
24 | next_run timestamp with time zone NOT NULL DEFAULT now(),
25 | created_at timestamp with time zone NOT NULL DEFAULT now()
26 | )
27 |
--------------------------------------------------------------------------------
/pkg/tlsmanage/renew_bg.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package tlsmanage
5 |
6 | import (
7 | "context"
8 | "time"
9 |
10 | "github.com/hashicorp/go-hclog"
11 | "github.com/hashicorp/horizon/pkg/workq"
12 | )
13 |
14 | var (
15 | HubCertRenewPeriod = time.Hour * 24 * 30 // every 30 days
16 | )
17 |
18 | func init() {
19 | workq.RegisterPeriodicJob("renew-hub-cert", "default", "renew-hub-cert", nil, HubCertRenewPeriod)
20 | }
21 |
22 | func (m *Manager) RegisterRenewHandler(L hclog.Logger, reg *workq.Registry) {
23 | reg.Register("renew-hub-cert", func(ctx context.Context, jobType string, _ *struct{}) error {
24 | err := m.SetupHubCert(ctx)
25 | if err != nil {
26 | L.Error("error retrieving updated cert/key for hub", "error", err)
27 | return err
28 | }
29 |
30 | err = m.StoreInVault()
31 | if err != nil {
32 | L.Error("error storing new cert/key in vault", "error", err)
33 | return err
34 | }
35 |
36 | return nil
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/workq/default.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "encoding/json"
8 | "sync"
9 | "time"
10 | )
11 |
12 | // A default registry that other packages can easily register their types
13 | // against.
14 | var GlobalRegistry = &Registry{}
15 |
16 | // Register a job and handler with the default registry.
17 | func RegisterHandler(jobType string, h interface{}) {
18 | GlobalRegistry.Register(jobType, h)
19 | }
20 |
21 | type defaultPeriodic struct {
22 | name, queue, jobType string
23 | payload []byte
24 | period time.Duration
25 | }
26 |
27 | var periodMu sync.Mutex
28 |
29 | var defaultPeriodics []defaultPeriodic
30 |
31 | func RegisterPeriodicJob(name, queue, jobType string, v interface{}, period time.Duration) {
32 | periodMu.Lock()
33 | defer periodMu.Unlock()
34 |
35 | payload, err := json.Marshal(v)
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | defaultPeriodics = append(defaultPeriodics, defaultPeriodic{
41 | name, queue, jobType, payload, period,
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax = docker.mirror.hashicorp.services/docker/dockerfile:experimental
2 | # Copyright (c) HashiCorp, Inc.
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 |
6 | FROM docker.mirror.hashicorp.services/golang:1.15-alpine AS builder
7 |
8 | RUN apk add --no-cache git gcc libc-dev
9 |
10 | RUN mkdir -p /tmp/hzn-prime
11 | COPY go.sum /tmp/hzn-prime
12 | COPY go.mod /tmp/hzn-prime
13 |
14 | WORKDIR /tmp/hzn-prime
15 |
16 | RUN go mod download
17 |
18 | COPY . /tmp/hzn-src
19 |
20 | WORKDIR /tmp/hzn-src
21 |
22 | RUN --mount=type=cache,target=/root/.cache/go-build go build -o /tmp/hzn -ldflags "-X main.sha1ver=`git rev-parse HEAD` -X main.buildTime=$(date +'+%FT%T.%N%:z')" ./cmd/hzn
23 | RUN --mount=type=cache,target=/root/.cache/go-build go build -o /tmp/hznctl -ldflags "-X main.sha1ver=`git rev-parse HEAD` -X main.buildTime=$(date +'+%FT%T.%N%:z')" ./cmd/hznctl
24 |
25 | FROM docker.mirror.hashicorp.services/alpine
26 |
27 | COPY --from=builder /tmp/hzn /usr/bin/hzn
28 | COPY --from=builder /tmp/hznctl /usr/bin/hznctl
29 |
30 | COPY ./pkg/control/migrations /migrations
31 |
32 | ENTRYPOINT ["/usr/bin/hzn"]
33 |
--------------------------------------------------------------------------------
/pkg/control/zstd.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | "bytes"
8 | "io"
9 | "sync"
10 |
11 | "github.com/klauspost/compress/zstd"
12 | )
13 |
14 | var zwriters = sync.Pool{
15 | New: func() interface{} {
16 | w, _ := zstd.NewWriter(nil)
17 | return w
18 | },
19 | }
20 |
21 | func zstdCompress(data []byte) ([]byte, error) {
22 | var buf bytes.Buffer
23 |
24 | w := zwriters.Get().(*zstd.Encoder)
25 | w.Reset(&buf)
26 |
27 | defer zwriters.Put(w)
28 |
29 | defer w.Close()
30 |
31 | if _, err := io.Copy(w, bytes.NewReader(data)); err != nil {
32 | return nil, err
33 | }
34 | if err := w.Close(); err != nil {
35 | return nil, err
36 | }
37 |
38 | return buf.Bytes(), nil
39 | }
40 |
41 | func zstdDecompress(data []byte) ([]byte, error) {
42 | var buf bytes.Buffer
43 | r, err := zstd.NewReader(bytes.NewReader(data))
44 | if err != nil {
45 | return nil, err
46 | }
47 | defer r.Close()
48 |
49 | if _, err := io.Copy(&buf, r); err != nil {
50 | return nil, err
51 | }
52 | r.Close()
53 |
54 | return buf.Bytes(), nil
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/web/tls.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package web
5 |
6 | import (
7 | "crypto/tls"
8 | "net/http"
9 |
10 | "github.com/caddyserver/certmagic"
11 | "github.com/hashicorp/go-hclog"
12 | )
13 |
14 | type TLS struct {
15 | cfg *certmagic.Config
16 | }
17 |
18 | func NewTLS(L hclog.Logger, path, email string, test bool, storage certmagic.Storage, decision func(name string) error) (*TLS, error) {
19 | certmagic.DefaultACME.Agreed = true
20 | certmagic.DefaultACME.DisableHTTPChallenge = true
21 |
22 | if test {
23 | certmagic.DefaultACME.CA = certmagic.DefaultACME.TestCA
24 | }
25 |
26 | certmagic.DefaultACME.Email = email
27 |
28 | cfg := certmagic.NewDefault()
29 | cfg.Storage = storage
30 | cfg.OnDemand = &certmagic.OnDemandConfig{
31 | DecisionFunc: decision,
32 | }
33 |
34 | return &TLS{
35 | cfg: cfg,
36 | }, nil
37 | }
38 |
39 | func (t *TLS) ListenAndServe(addr string, h http.Handler) error {
40 | listener, err := tls.Listen("tcp", addr, t.cfg.TLSConfig())
41 | if err != nil {
42 | return err
43 | }
44 |
45 | return http.Serve(listener, h)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/discovery/hub.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package discovery
5 |
6 | import (
7 | "context"
8 | "crypto/x509"
9 | "sync"
10 | )
11 |
12 | type HubConfig struct {
13 | Addr string
14 | Name string
15 | Insecure bool
16 | PinnedCert *x509.Certificate
17 | }
18 |
19 | type HubConnectDetails interface {
20 | Address() string
21 | InsecureTLS() bool
22 | X509Cert() *x509.Certificate
23 | }
24 |
25 | type HubConfigProvider interface {
26 | Take(context.Context) (HubConfig, bool)
27 | Return(HubConfig)
28 | }
29 |
30 | type StaticHubConfigs struct {
31 | mu sync.Mutex
32 | configs []HubConfig
33 | }
34 |
35 | func HubConfigs(cfg ...HubConfig) HubConfigProvider {
36 | return &StaticHubConfigs{configs: cfg}
37 | }
38 |
39 | func (h *StaticHubConfigs) Take(ctx context.Context) (HubConfig, bool) {
40 | h.mu.Lock()
41 | defer h.mu.Unlock()
42 |
43 | if len(h.configs) == 0 {
44 | return HubConfig{}, false
45 | }
46 |
47 | cfg := h.configs[0]
48 | h.configs = h.configs[1:]
49 |
50 | return cfg, true
51 | }
52 |
53 | func (h *StaticHubConfigs) Return(cfg HubConfig) {
54 | h.mu.Lock()
55 | defer h.mu.Unlock()
56 |
57 | h.configs = append(h.configs, cfg)
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/pb/label.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: label.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *Label) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *Label) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *LabelSet) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *LabelSet) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/tlsmanage/vault.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package tlsmanage
5 |
6 | import (
7 | "encoding/base64"
8 |
9 | "github.com/pkg/errors"
10 | )
11 |
12 | var ErrNoTLSMaterial = errors.New("no tls material available")
13 |
14 | func (m *Manager) FetchFromVault() ([]byte, []byte, error) {
15 | sec, err := m.cfg.VaultClient.Logical().Read("/kv/data/hub-tls")
16 | if err != nil {
17 | return nil, nil, err
18 | }
19 |
20 | if sec == nil {
21 | return nil, nil, ErrNoTLSMaterial
22 | }
23 |
24 | data, ok := sec.Data["data"].(map[string]interface{})
25 | if !ok {
26 | return nil, nil, ErrNoTLSMaterial
27 | }
28 |
29 | key, err := base64.StdEncoding.DecodeString(data["key"].(string))
30 | if err != nil {
31 | return nil, nil, err
32 | }
33 |
34 | cert, err := base64.StdEncoding.DecodeString(data["certificate"].(string))
35 | if err != nil {
36 | return nil, nil, err
37 | }
38 |
39 | return cert, key, nil
40 | }
41 |
42 | func (m *Manager) StoreInVault() error {
43 | _, err := m.cfg.VaultClient.Logical().Write("/kv/data/hub-tls", map[string]interface{}{
44 | "data": map[string]interface{}{
45 | "key": m.hubKey,
46 | "certificate": m.hubCert,
47 | },
48 | })
49 |
50 | return err
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/token/vault_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/hashicorp/horizon/pkg/pb"
10 | "github.com/hashicorp/horizon/pkg/testutils"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestVault(t *testing.T) {
16 | vc := testutils.SetupVault()
17 |
18 | t.Run("can setup a key and use it", func(t *testing.T) {
19 | id := pb.NewULID().SpecString()
20 |
21 | pub, err := SetupVault(vc, id)
22 | require.NoError(t, err)
23 |
24 | var tc TokenCreator
25 | tc.AccountId = pb.NewULID()
26 | tc.AccuntNamespace = "/test"
27 | tc.Capabilities = map[pb.Capability]string{
28 | pb.CONNECT: "",
29 | pb.SERVE: "",
30 | }
31 |
32 | stoken, err := tc.EncodeED25519WithVault(vc, id, "k1")
33 | require.NoError(t, err)
34 |
35 | vt, err := CheckTokenED25519(stoken, pub)
36 | require.NoError(t, err)
37 |
38 | cb := func(ok bool, _ string) bool {
39 | return ok
40 | }
41 |
42 | assert.True(t, cb(vt.HasCapability(pb.CONNECT)))
43 | assert.True(t, cb(vt.HasCapability(pb.SERVE)))
44 | assert.False(t, cb(vt.HasCapability(pb.ACCESS)))
45 | assert.Equal(t, "k1", vt.KeyId)
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/control/server_http_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | "encoding/json"
8 | "net/http"
9 | "net/http/httptest"
10 | "os"
11 | "path/filepath"
12 | "testing"
13 |
14 | "github.com/oschwald/geoip2-golang"
15 | "github.com/stretchr/testify/assert"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | func TestServerHTTP(t *testing.T) {
20 | t.Run("can perform asn lookups on the ip", func(t *testing.T) {
21 | path := filepath.Join("..", "..", "tmp", "GeoLite2-ASN.mmdb")
22 |
23 | _, err := os.Stat(path)
24 | if err != nil {
25 | t.Skip("missing geolite database")
26 | }
27 |
28 | var s Server
29 |
30 | db, err := geoip2.Open(path)
31 | require.NoError(t, err)
32 |
33 | s.asnDB = db
34 |
35 | req, err := http.NewRequest("GET", "/ip-info", nil)
36 | require.NoError(t, err)
37 |
38 | req.Header.Add("X-Real-IP", "1.1.1.1")
39 |
40 | w := httptest.NewRecorder()
41 | s.httpIPInfo(w, req)
42 |
43 | require.Equal(t, 200, w.Code)
44 |
45 | var info ipInfo
46 |
47 | err = json.Unmarshal(w.Body.Bytes(), &info)
48 | require.NoError(t, err)
49 |
50 | assert.Equal(t, "AS13335", info.ASN)
51 | assert.Equal(t, "CLOUDFLARENET", info.ASNOrg)
52 | })
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/pb/ulid.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: ulid.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *ULID) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *ULID) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *ULIDWithDuration) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *ULIDWithDuration) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/pb/account.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: account.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *Account) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *Account) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *Account_Limits) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *Account_Limits) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/token/token.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | strings "strings"
8 |
9 | "github.com/hashicorp/horizon/pkg/pb"
10 | )
11 |
12 | func (t *ValidToken) Account() *pb.Account {
13 | return t.Body.Account
14 | }
15 |
16 | func (t *ValidToken) HasCapability(target pb.Capability) (bool, string) {
17 | for _, capa := range t.Body.Capabilities {
18 | if capa.Capability == target {
19 | return true, capa.Value
20 | }
21 | }
22 |
23 | return false, ""
24 | }
25 |
26 | func (t *ValidToken) AllowAccount(ns string) bool {
27 | // First, this token has to have the capability to access other accounts
28 | ok, val := t.HasCapability(pb.ACCESS)
29 | if !ok {
30 | return false
31 | }
32 |
33 | // Then if the access namespace is the same as the requestd namespace, allow
34 | // it.
35 | if ns == val {
36 | return true
37 | }
38 |
39 | // If the access namespace is not a valid prefix of requested one, then def
40 | if !strings.HasPrefix(ns, val) {
41 | return false
42 | }
43 |
44 | // Verify that after the prefix is a separater so that the access namespace
45 | // doesn't accidentally match a partial namespace
46 | if ns[len(val)] != '/' {
47 | return false
48 | }
49 |
50 | return true
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/pb/token.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "defs/gogo.proto";
7 | import "timestamp.proto";
8 | import "kv.proto";
9 | import "ulid.proto";
10 | import "account.proto";
11 |
12 | package pb;
13 |
14 | message Headers {
15 | repeated KVPair headers = 1;
16 | }
17 |
18 | message Signature {
19 | enum SigType {
20 | BLAKE2HMAC = 0;
21 | ED25519 = 1;
22 | EXTERNAL = 2;
23 | }
24 |
25 | bytes signature = 1;
26 | SigType sig_type = 2;
27 | string key_id = 3;
28 | Headers headers = 4;
29 | }
30 |
31 | enum Capability {
32 | CONNECT = 0;
33 | SERVE = 1;
34 | ACCESS = 2;
35 | MGMT = 3;
36 | CONFIG = 4;
37 | }
38 |
39 | message TokenCapability {
40 | Capability capability = 1;
41 | string value = 2;
42 | }
43 |
44 | enum TokenRole {
45 | AGENT = 0;
46 | HUB = 1;
47 | MANAGE = 2;
48 | }
49 |
50 | message Token {
51 | message Body {
52 | TokenRole role = 1;
53 | ULID id = 2;
54 | Account account = 3;
55 | Timestamp valid_until = 4;
56 | repeated TokenCapability capabilities = 5 [(gogoproto.nullable) = false];
57 |
58 | Headers additional = 10;
59 | }
60 |
61 | bytes body = 1;
62 | Headers metadata = 2;
63 | repeated Signature signatures = 3;
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/agent/query.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "github.com/hashicorp/horizon/pkg/wire"
8 | )
9 |
10 | func (a *Agent) RPCClient() (*wire.RPCClient, error) {
11 | a.mu.Lock()
12 | stream, err := a.sessions[0].OpenStream()
13 | a.mu.Unlock()
14 |
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return wire.NewRPCClient(stream), nil
20 | }
21 |
22 | /*
23 |
24 | func (a *Agent) QueryPeerService(labels []string) ([]*agents.Service, error) {
25 | rpc, err := a.RPCClient()
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | var (
31 | req agents.QueryRequest
32 | resp agents.QueryResponse
33 | )
34 |
35 | req.Labels = labels
36 |
37 | err = rpc.Call("agents.edge", "/query/peers", &req, &resp)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | return resp.Services, nil
43 | }
44 |
45 | func (a *Agent) ConnectToPeer(serv *agents.Service) (wire.Context, error) {
46 | rpc, err := a.RPCClient()
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | var req agents.ConnectRequest
52 | req.Target = serv
53 |
54 | wctx, err := rpc.Begin("agents.edge", "/connect/peer", &req)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | return wctx, nil
60 | }
61 |
62 | */
63 |
--------------------------------------------------------------------------------
/pkg/wire/framing_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | bytes "bytes"
8 | "io"
9 | "io/ioutil"
10 | "testing"
11 |
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | "github.com/pierrec/lz4/v3"
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestFraming(t *testing.T) {
19 | t.Run("handles running over lz4 properly", func(t *testing.T) {
20 | var out bytes.Buffer
21 |
22 | w := lz4.NewWriter(&out)
23 | fw, err := NewFramingWriter(w)
24 | require.NoError(t, err)
25 |
26 | var sid pb.SessionIdentification
27 | sid.ServiceId = pb.NewULID()
28 | sid.ProtocolId = "blah"
29 |
30 | _, err = fw.WriteMarshal(11, &sid)
31 | require.NoError(t, err)
32 |
33 | mb := MarshalBytes("hello hzn!")
34 | _, err = fw.WriteMarshal(30, &mb)
35 | require.NoError(t, err)
36 |
37 | r := lz4.NewReader(bytes.NewReader(out.Bytes()))
38 |
39 | fr, err := NewFramingReader(r)
40 | require.NoError(t, err)
41 |
42 | tag, _, err := fr.Next()
43 | require.NoError(t, err)
44 |
45 | assert.Equal(t, byte(11), tag)
46 |
47 | io.Copy(ioutil.Discard, fr)
48 |
49 | tag, _, err = fr.Next()
50 | require.NoError(t, err)
51 |
52 | assert.Equal(t, byte(30), tag)
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/agent/tcp.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "io"
10 | "net"
11 | "sync"
12 |
13 | "github.com/hashicorp/go-hclog"
14 | "github.com/hashicorp/horizon/pkg/pb"
15 | )
16 |
17 | type tcpHandler struct {
18 | addr string
19 | }
20 |
21 | func TCPHandler(addr string) ServiceHandler {
22 | return &tcpHandler{addr}
23 | }
24 |
25 | func (h *tcpHandler) HandleRequest(ctx context.Context, L hclog.Logger, sctx ServiceContext) error {
26 | defer sctx.Close()
27 | proto := sctx.ProtocolId()
28 |
29 | if !(proto == "" || proto == "tcp") {
30 | return fmt.Errorf("unknown protocol: %s", proto)
31 | }
32 |
33 | c, err := net.Dial("tcp", h.addr)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | id := pb.NewULID()
39 |
40 | L.Trace("tcp session started", "id", id, "addr", h.addr, "session-addr", c.LocalAddr())
41 |
42 | r := sctx.Reader()
43 | w := sctx.Writer()
44 |
45 | var wg sync.WaitGroup
46 | wg.Add(2)
47 |
48 | go func() {
49 | defer wg.Done()
50 | defer w.Close()
51 |
52 | io.Copy(w, c)
53 | }()
54 |
55 | go func() {
56 | defer wg.Done()
57 | defer c.Close()
58 |
59 | io.Copy(c, r)
60 | }()
61 |
62 | wg.Wait()
63 |
64 | L.Trace("tcp session ended", "id", id)
65 |
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/config/db.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package config
5 |
6 | import (
7 | "os"
8 | "strings"
9 | "sync"
10 |
11 | "github.com/jinzhu/gorm"
12 | "github.com/mitchellh/go-testing-interface"
13 |
14 | "github.com/hashicorp/horizon/internal/testsql"
15 | )
16 |
17 | var (
18 | dbOnce sync.Once
19 | db *gorm.DB
20 | )
21 |
22 | var (
23 | TestDBUrl = "postgres://localhost/horizon_test?sslmode=disable"
24 | DevDBUrl = "postgres://localhost/horizon_dev?sslmode=disable"
25 | )
26 |
27 | func DB() *gorm.DB {
28 | dbOnce.Do(func() {
29 | if db != nil {
30 | return
31 | }
32 |
33 | // If we're in a unit test, use the test framework to create the
34 | // DB. This is safe even if the heuristic is wrong because we always
35 | // create a new test database. So anything in DATABASE_URL is safe.
36 | if strings.HasSuffix(os.Args[0], ".test") {
37 | db = testsql.TestPostgresDB(&testing.RuntimeT{}, "hzn_config")
38 | return
39 | }
40 |
41 | connect := os.Getenv("DATABASE_URL")
42 | if connect == "" {
43 | panic("DATABASE_URL must be set")
44 | }
45 |
46 | x, err := gorm.Open("postgres", connect)
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | db = x
52 | })
53 |
54 | if db == nil {
55 | panic("no database configured")
56 | }
57 | return db
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/token/vault.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "crypto/ed25519"
8 | "encoding/base64"
9 | "fmt"
10 | "path/filepath"
11 |
12 | "github.com/hashicorp/vault/api"
13 | "github.com/mitchellh/mapstructure"
14 | )
15 |
16 | func SetupVault(vc *api.Client, path string) (ed25519.PublicKey, error) {
17 | sec, err := vc.Logical().Read(filepath.Join("/transit/keys", path))
18 | if err != nil {
19 | return nil, err
20 | }
21 |
22 | if sec == nil {
23 | _, err = vc.Logical().Write(filepath.Join("/transit/keys", path), map[string]interface{}{
24 | "type": "ed25519",
25 | })
26 |
27 | sec, err = vc.Logical().Read(filepath.Join("/transit/keys", path))
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | if sec == nil {
33 | return nil, fmt.Errorf("vault transit not available")
34 | }
35 | }
36 |
37 | type keyData struct {
38 | PublicKey string `mapstructure:"public_key"`
39 | }
40 |
41 | var secData struct {
42 | Keys map[string]keyData `mapstructure:"keys"`
43 | }
44 |
45 | err = mapstructure.Decode(sec.Data, &secData)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | key, err := base64.StdEncoding.DecodeString(secData.Keys["1"].PublicKey)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | return key, nil
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/discovery/server.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package discovery
5 |
6 | import (
7 | "encoding/json"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/hashicorp/go-hclog"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | )
14 |
15 | const HTTPPath = "/.well-known/horizon/hubs.json"
16 |
17 | type GetNetlocs interface {
18 | GetAllNetworkLocations() ([]*pb.NetworkLocation, error)
19 | }
20 |
21 | type WellKnown struct {
22 | L hclog.Logger
23 | GetNetlocs GetNetlocs
24 | }
25 |
26 | type DiscoveryData struct {
27 | ServerTime time.Time `json:"server_time"`
28 | Hubs []*pb.NetworkLocation `json:"hubs"`
29 | }
30 |
31 | func (wk *WellKnown) ServeHTTP(w http.ResponseWriter, req *http.Request) {
32 | netlocs, err := wk.GetNetlocs.GetAllNetworkLocations()
33 | if err != nil {
34 | wk.L.Error("error getting network locations for well-known", "error", err)
35 | http.Error(w, "unable to find network locations", http.StatusInternalServerError)
36 | return
37 | }
38 |
39 | var dd DiscoveryData
40 | dd.ServerTime = time.Now()
41 |
42 | for _, loc := range netlocs {
43 | if loc.IsPublic() {
44 | dd.Hubs = append(dd.Hubs, loc)
45 | }
46 | }
47 |
48 | err = json.NewEncoder(w).Encode(&dd)
49 | if err != nil {
50 | wk.L.Error("error encoding discovery data for well-known", "error", err)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/control/labels.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | fmt "fmt"
8 | "sort"
9 | "strings"
10 |
11 | "github.com/hashicorp/horizon/pkg/pb"
12 | )
13 |
14 | func FlattenLabels(labels *pb.LabelSet) string {
15 | sort.Sort(labels)
16 |
17 | var out []string
18 |
19 | for _, lbl := range labels.Labels {
20 | out = append(out, fmt.Sprintf("%s=%s", lbl.Name, lbl.Value))
21 | }
22 |
23 | return strings.Join(out, ",")
24 | }
25 |
26 | func FlattenLabelSets(sets []*pb.LabelSet) []string {
27 | var out []string
28 |
29 | for _, set := range sets {
30 | out = append(out, FlattenLabels(set))
31 | }
32 |
33 | return out
34 | }
35 |
36 | func ExplodeLabelSetss(in []string) []*pb.LabelSet {
37 | var ret []*pb.LabelSet
38 |
39 | for _, list := range in {
40 | ret = append(ret, ExplodeLabels(list))
41 | }
42 |
43 | return ret
44 | }
45 |
46 | func ExplodeLabels(list string) *pb.LabelSet {
47 | var set pb.LabelSet
48 | for _, pair := range strings.Split(list, ",") {
49 | var name, val string
50 |
51 | eqIdx := strings.IndexByte(pair, '=')
52 | if eqIdx != -1 {
53 | name = pair[:eqIdx]
54 | val = pair[eqIdx+1:]
55 | } else {
56 | name = pair
57 | }
58 |
59 | set.Labels = append(set.Labels, &pb.Label{
60 | Name: name,
61 | Value: val,
62 | })
63 | }
64 |
65 | return &set
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/pb/flow.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "ulid.proto";
7 | import "timestamp.proto";
8 | import "label.proto";
9 | import "account.proto";
10 |
11 | package pb;
12 |
13 | message FlowStream {
14 | ULID flow_id = 1;
15 | ULID hub_id = 2;
16 | ULID agent_id = 3;
17 | ULID service_id = 4;
18 | Account account = 5;
19 | LabelSet labels = 6;
20 |
21 | Timestamp started_at = 10;
22 | Timestamp ended_at = 11;
23 |
24 | int64 num_messages = 12;
25 | int64 num_bytes = 13;
26 |
27 | int64 duration = 14;
28 | }
29 |
30 | message FlowRecord {
31 | message AgentConnection {
32 | ULID hub_id = 1;
33 | ULID agent_id = 2;
34 | Account account = 3;
35 |
36 | Timestamp started_at = 10;
37 | Timestamp ended_at = 11;
38 |
39 | int32 num_services = 12;
40 | int64 active_streams = 13;
41 | }
42 |
43 | AgentConnection agent = 1;
44 |
45 | FlowStream stream = 2;
46 |
47 | message HubStats {
48 | ULID hub_id = 1;
49 | int64 active_agents = 2;
50 | int64 total_agents = 3;
51 | int64 services = 4;
52 | }
53 |
54 | HubStats hub_stats = 3;
55 | }
56 |
57 | message FlowTopSnapshot {
58 | repeated FlowStream records = 1;
59 | }
60 |
61 | message FlowTopRequest {
62 | int32 max_records = 1;
63 | }
64 |
65 | service FlowTopReporter {
66 | rpc CurrentFlowTop(FlowTopRequest) returns (FlowTopSnapshot) {}
67 | }
68 |
--------------------------------------------------------------------------------
/internal/httpassets/static/error_limit.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 | HashiCorp Waypoint URL Service
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 | Usage limits exceeded.
26 | The configured limits for this account have been exceeded.
27 | Time until next request can be performed: %s
28 |
29 | You can also refer to the
30 | documentation
31 | for help with Waypoint.
32 |
33 |
34 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/pkg/testutils/s3.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package testutils
5 |
6 | import (
7 | "strings"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/aws/credentials"
11 | "github.com/aws/aws-sdk-go/aws/session"
12 | "github.com/aws/aws-sdk-go/service/s3"
13 | "github.com/mitchellh/go-testing-interface"
14 | )
15 |
16 | func AWSSession(t testing.T) *session.Session {
17 | return session.New(aws.NewConfig().
18 | WithEndpoint("http://localhost:4566").
19 | WithRegion("us-east-1").
20 | WithCredentials(credentials.NewStaticCredentials("hzn", "hzn", "hzn")).
21 | WithS3ForcePathStyle(true),
22 | )
23 | }
24 |
25 | func DeleteBucket(api *s3.S3, bucket string) {
26 | var marker *string
27 |
28 | for {
29 | objects, err := api.ListObjects(&s3.ListObjectsInput{
30 | Bucket: aws.String(bucket),
31 | Marker: marker,
32 | })
33 |
34 | if err != nil {
35 | // Deleting a non-existent bucket is not a bug
36 | if strings.Contains(err.Error(), "NoSuchBucket") {
37 | break
38 | }
39 |
40 | panic(err)
41 | }
42 |
43 | if len(objects.Contents) == 0 {
44 | break
45 | }
46 |
47 | marker = objects.NextMarker
48 |
49 | for _, obj := range objects.Contents {
50 | _, err = api.DeleteObject(&s3.DeleteObjectInput{
51 | Bucket: aws.String(bucket),
52 | Key: obj.Key,
53 | })
54 | }
55 | }
56 |
57 | api.DeleteBucket(&s3.DeleteBucketInput{
58 | Bucket: aws.String(bucket),
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/pb/ulid.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | import (
7 | "crypto/rand"
8 | "time"
9 |
10 | "github.com/oklog/ulid"
11 | )
12 |
13 | var mrand = rand.Reader
14 |
15 | func NewULID() *ULID {
16 | id := ulid.MustNew(ulid.Now(), mrand)
17 |
18 | return &ULID{
19 | Timestamp: id.Time(),
20 | Entropy: id.Entropy(),
21 | }
22 | }
23 |
24 | func ULIDFromBytes(b []byte) *ULID {
25 | var id ulid.ULID
26 | copy(id[:], b)
27 |
28 | return &ULID{
29 | Timestamp: id.Time(),
30 | Entropy: id.Entropy(),
31 | }
32 | }
33 |
34 | func ParseULID(s string) (*ULID, error) {
35 | id, err := ulid.Parse(s)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | return &ULID{
41 | Timestamp: id.Time(),
42 | Entropy: id.Entropy(),
43 | }, nil
44 | }
45 |
46 | // Used when generating internal tokens for use by hub services
47 | var InternalAccount *ULID
48 |
49 | func init() {
50 | InternalAccount, _ = ParseULID("01E7KKMY4HKNZWATXC6SDQ1V4D")
51 | }
52 |
53 | func (u *ULID) Time() time.Time {
54 | return ulid.Time(u.Timestamp)
55 | }
56 |
57 | func (u *ULID) native() ulid.ULID {
58 | var ux ulid.ULID
59 |
60 | ux.SetTime(u.Timestamp)
61 | copy(ux[6:], u.Entropy)
62 |
63 | return ux
64 | }
65 |
66 | func (u *ULID) SpecString() string {
67 | return u.native().String()
68 | }
69 |
70 | func (u *ULID) String() string {
71 | return u.SpecString()
72 | }
73 |
74 | func (u ULID) Bytes() []byte {
75 | x := u.native()
76 | return x[:]
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/workq/registry_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "context"
8 | "encoding/json"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestRegistry(t *testing.T) {
16 | t.Run("calls a handler func with the job value", func(t *testing.T) {
17 |
18 | type foo struct {
19 | Name string
20 | Age int
21 | }
22 |
23 | f := func(ctx context.Context, jt string, f *foo) error {
24 | assert.Equal(t, jt, "foo_happened")
25 | assert.Equal(t, "boo", f.Name)
26 | assert.Equal(t, 42, f.Age)
27 | return nil
28 | }
29 |
30 | var r Registry
31 |
32 | r.Register("foo_happened", f)
33 |
34 | data, err := json.Marshal(&foo{Name: "boo", Age: 42})
35 | require.NoError(t, err)
36 |
37 | err = r.Handle(context.TODO(), &Job{
38 | JobType: "foo_happened",
39 | Payload: data,
40 | })
41 |
42 | require.NoError(t, err)
43 | })
44 |
45 | t.Run("can handle an empty struct", func(t *testing.T) {
46 |
47 | f := func(ctx context.Context, jt string, f *struct{}) error {
48 | assert.Equal(t, jt, "foo_happened")
49 | return nil
50 | }
51 |
52 | var r Registry
53 |
54 | r.Register("foo_happened", f)
55 |
56 | data, err := json.Marshal(nil)
57 | require.NoError(t, err)
58 |
59 | err = r.Handle(context.TODO(), &Job{
60 | JobType: "foo_happened",
61 | Payload: data,
62 | })
63 |
64 | require.NoError(t, err)
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/workq/periodic.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "time"
8 |
9 | "github.com/hashicorp/horizon/pkg/dbx"
10 | "github.com/jinzhu/gorm"
11 | )
12 |
13 | type PeriodicJob struct {
14 | Id int `gorm:"primary_key"`
15 | Name string
16 | Queue string
17 | JobType string
18 | Payload []byte
19 | Period string
20 | NextRun time.Time
21 |
22 | CreatedAt time.Time
23 | }
24 |
25 | func (w *Worker) CheckPeriodic() error {
26 | // We churn this loop until there are no more periodic jobs to schedule
27 | for {
28 | tx := w.db.Begin()
29 |
30 | var pjob PeriodicJob
31 |
32 | err := dbx.Check(
33 | tx.
34 | Set("gorm:query_option", "FOR UPDATE SKIP LOCKED").
35 | Where("next_run <= now()").
36 | First(&pjob),
37 | )
38 |
39 | if err != nil {
40 | tx.Rollback()
41 | if err == gorm.ErrRecordNotFound {
42 | return nil
43 | }
44 |
45 | return err
46 | }
47 |
48 | dur, err := time.ParseDuration(pjob.Period)
49 | if err != nil {
50 | tx.Rollback()
51 | return err
52 | }
53 |
54 | tx.Model(&pjob).Update("next_run", time.Now().Add(dur))
55 |
56 | job := NewJob()
57 | job.Queue = pjob.Queue
58 | job.Payload = pjob.Payload
59 | job.JobType = pjob.JobType
60 |
61 | tx.Create(&job)
62 |
63 | w.L.Info("queued job via periodic job", "name", pjob.Name, "queue", pjob.Queue, "job-type", pjob.JobType)
64 |
65 | err = dbx.Check(tx.Commit())
66 | if err != nil {
67 | return err
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/internal/httpassets/static/error.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 | HashiCorp Waypoint URL Service
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 | Couldn't find a Waypoint deployment with this URL.
26 |
27 | If you expected to see a deployment here you might want to check your
28 | logs to see if something went wrong.
29 |
30 |
31 | You can also refer to the
32 | documentation
33 | for help with Waypoint.
34 |
35 |
36 | Error reason: %s
37 |
38 |
39 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/pkg/wire/ulid.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | bytes "bytes"
8 | "encoding/json"
9 |
10 | "github.com/oklog/ulid"
11 | )
12 |
13 | type ULID struct {
14 | ulid.ULID
15 | }
16 |
17 | func (u ULID) Bytes() []byte {
18 | return u.ULID[:]
19 | }
20 |
21 | func (u ULID) Marshal() ([]byte, error) {
22 | return u.MarshalBinary()
23 | }
24 |
25 | func (u ULID) MarshalTo(data []byte) (n int, err error) {
26 | if len(u.ULID) == 0 {
27 | return 0, nil
28 | }
29 | copy(data, u.ULID[:])
30 | return 16, nil
31 | }
32 |
33 | func (u *ULID) Unmarshal(data []byte) error {
34 | if len(data) == 0 {
35 | u = nil
36 | return nil
37 | }
38 |
39 | copy(u.ULID[:], data)
40 |
41 | return nil
42 | }
43 |
44 | func (u *ULID) Size() int {
45 | if u == nil {
46 | return 0
47 | }
48 | if len(u.ULID) == 0 {
49 | return 0
50 | }
51 | return 16
52 | }
53 |
54 | func (u ULID) MarshalJSON() ([]byte, error) {
55 | return json.Marshal(u.String())
56 | }
57 |
58 | func (u *ULID) UnmarshalJSON(data []byte) error {
59 | var s string
60 | err := json.Unmarshal(data, &s)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | u.ULID, err = ulid.Parse(s)
66 | return err
67 | }
68 |
69 | func (u ULID) Equal(other ULID) bool {
70 | return bytes.Equal(u.ULID[0:], other.ULID[0:])
71 | }
72 |
73 | func (u ULID) Compare(other ULID) int {
74 | return bytes.Compare(u.ULID[0:], other.ULID[0:])
75 | }
76 |
77 | func ULIDFromBytes(b []byte) ULID {
78 | var u ULID
79 | copy(u.ULID[:], b)
80 | return u
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/workq/injector.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "database/sql"
8 | "encoding/json"
9 | "time"
10 |
11 | "github.com/hashicorp/horizon/pkg/dbx"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | "github.com/jinzhu/gorm"
14 | )
15 |
16 | type Injector struct {
17 | db *gorm.DB
18 | }
19 |
20 | func (i *Injector) Inject(job *Job) error {
21 | if job.Id == nil {
22 | job.Id = pb.NewULID().Bytes()
23 | }
24 |
25 | tx := i.db.Begin()
26 |
27 | tx.Create(&job)
28 |
29 | tx.Exec("NOTIFY " + listenChannel)
30 |
31 | return dbx.Check(tx.Commit())
32 | }
33 |
34 | func (i *Injector) AddPeriodicJob(name, queue, jt string, v interface{}, period time.Duration) error {
35 | data, err := json.Marshal(v)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | return i.AddPeriodicJobRaw(name, queue, jt, data, period)
41 | }
42 |
43 | func (i *Injector) AddPeriodicJobRaw(name, queue, jt string, payload []byte, period time.Duration) error {
44 | var pjob PeriodicJob
45 |
46 | pjob.Name = name
47 | pjob.Queue = queue
48 | pjob.Period = period.String()
49 | pjob.JobType = jt
50 | pjob.NextRun = time.Now().Add(period)
51 | pjob.Payload = payload
52 |
53 | err := dbx.Check(
54 | i.db.Set("gorm:insert_option",
55 | "ON CONFLICT (name) DO UPDATE SET queue=EXCLUDED.queue, payload=EXCLUDED.payload, period=EXCLUDED.period, next_run=LEAST(periodic_jobs.next_run, EXCLUDED.next_run)").
56 | Create(&pjob),
57 | )
58 |
59 | if err == sql.ErrNoRows {
60 | return nil
61 | }
62 |
63 | return err
64 | }
65 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | version: "3"
5 |
6 | services:
7 | pebble:
8 | image: docker.mirror.hashicorp.services/letsencrypt/pebble
9 | command: pebble
10 | ports:
11 | - 14000:14000 # ACME port
12 | - 15000:15000 # Management port
13 | environment:
14 | - PEBBLE_VA_NOSLEEP=1
15 | - PEBBLE_VA_ALWAYS_VALID=1
16 |
17 | localstack:
18 | image: docker.mirror.hashicorp.services/localstack/localstack
19 | ports:
20 | - "4566-4599:4566-4599"
21 | - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
22 | environment:
23 | - SERVICES=${SERVICES- }
24 | - DEBUG=${DEBUG- }
25 | - DATA_DIR=${DATA_DIR- }
26 | - PORT_WEB_UI=${PORT_WEB_UI- }
27 | - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
28 | - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
29 | - LAMBDA_EXECUTOR=local
30 | - HOST_TMP_FOLDER=${TMPDIR}
31 | volumes:
32 | - "${PWD}/tmp/localstack:/tmp/localstack"
33 |
34 | postgres:
35 | image: "docker.mirror.hashicorp.services/postgres:12.3"
36 | ports:
37 | - "5432:5432"
38 | environment:
39 | - POSTGRES_DB=noop
40 | - POSTGRES_PASSWORD=postgres
41 |
42 | vault:
43 | image: docker.mirror.hashicorp.services/vault
44 | ports:
45 | - 8200:8200
46 | command: vault server -dev -dev-root-token-id=hznroot
47 | environment:
48 | - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200
49 |
50 | consul:
51 | image: docker.mirror.hashicorp.services/consul
52 | ports:
53 | - 8500:8500
54 | command: consul agent -dev -client=0.0.0.0
55 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Let us know about a bug!
4 | title: ''
5 | labels: new
6 | assignees: ''
7 |
8 | ---
9 |
10 |
17 |
18 | **Describe the bug**
19 | A clear and concise description of what the bug is.
20 |
21 | **Steps to Reproduce**
22 | Steps to reproduce the behavior.
23 |
24 | Please include any `waypoint.hcl` files if applicable, as well as a
25 | [GitHub Gist](https://gist.github.com/) of any relevant logs or steps to
26 | reproduce the bug. Running `waypoint` commands with `-v` up to `-vvv` will
27 | include any additional debugging info in the log.
28 |
29 | **Expected behavior**
30 | A clear and concise description of what you expected to happen.
31 |
32 | **Waypoint Platform Versions**
33 | Additional version and platform information to help triage the issue if
34 | applicable:
35 |
36 | * Waypoint CLI Version:
37 | * Waypoint Server Platform and Version: (like `docker`, `nomad`, `kubernetes`)
38 | * Waypoint Plugin: (like `aws/ecs`, `pack`, `azure`)
39 |
40 | **Additional context**
41 | Add any other context about the problem here.
42 |
--------------------------------------------------------------------------------
/pkg/wire/adapt.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "io"
8 |
9 | "github.com/pkg/errors"
10 | )
11 |
12 | const (
13 | adaptTagEOF = 240
14 | adaptTagData = 241
15 | )
16 |
17 | type WriteAdapter struct {
18 | FW *FramingWriter
19 | }
20 |
21 | func (f *WriteAdapter) Write(b []byte) (int, error) {
22 | err := f.FW.WriteFrame(adaptTagData, len(b))
23 | if err != nil {
24 | return 0, err
25 | }
26 |
27 | return f.FW.Write(b)
28 | }
29 |
30 | func (f *WriteAdapter) Close() error {
31 | return f.FW.WriteFrame(adaptTagEOF, 0)
32 | }
33 |
34 | type ReadAdapter struct {
35 | FR *FramingReader
36 | rest int
37 | closed bool
38 | }
39 |
40 | var ErrProtocolError = errors.New("protocol error detected")
41 |
42 | func (f *ReadAdapter) Read(b []byte) (int, error) {
43 | if f.closed {
44 | return 0, io.EOF
45 | }
46 |
47 | if f.rest > 0 {
48 | if f.rest < len(b) {
49 | n := f.rest
50 | f.rest = 0
51 | return io.ReadFull(f.FR, b[:n])
52 | } else {
53 | n := len(b)
54 | f.rest -= n
55 | return io.ReadFull(f.FR, b)
56 | }
57 | }
58 |
59 | tag, sz, err := f.FR.Next()
60 | if err != nil {
61 | return 0, err
62 | }
63 |
64 | if tag == adaptTagEOF {
65 | f.closed = true
66 | return 0, io.EOF
67 | }
68 |
69 | if tag != adaptTagData {
70 | return 0, errors.Wrapf(ErrProtocolError, "wrong tag detected: %d (wanted %d)", tag, adaptTagData)
71 | }
72 |
73 | if sz < len(b) {
74 | return io.ReadFull(f.FR, b[:sz])
75 | }
76 |
77 | n, err := io.ReadFull(f.FR, b)
78 | if err != nil {
79 | return n, err
80 | }
81 |
82 | f.rest = sz - n
83 |
84 | return n, nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/timing/timing.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package timing
5 |
6 | import (
7 | "context"
8 | "sync"
9 | "time"
10 | )
11 |
12 | type trackVal struct{}
13 |
14 | type Metric interface {
15 | Start() Metric
16 | Stop()
17 | }
18 |
19 | type Span struct {
20 | Name string
21 | Duration time.Duration
22 | }
23 |
24 | type Tracker interface {
25 | NewMetric(name string) Metric
26 | Spans() []Span
27 | }
28 |
29 | type DefaultMetric struct {
30 | Name string
31 | StartTime time.Time
32 | StopTime time.Time
33 | }
34 |
35 | func (d *DefaultMetric) Start() Metric {
36 | d.StartTime = time.Now()
37 | return d
38 | }
39 |
40 | func (d *DefaultMetric) Stop() {
41 | d.StopTime = time.Now()
42 | }
43 |
44 | type DefaultTracker struct {
45 | mu sync.Mutex
46 | metrics []*DefaultMetric
47 | }
48 |
49 | func (d *DefaultTracker) NewMetric(name string) Metric {
50 | m := &DefaultMetric{Name: name}
51 |
52 | d.mu.Lock()
53 | defer d.mu.Unlock()
54 |
55 | d.metrics = append(d.metrics, m)
56 |
57 | return m
58 | }
59 |
60 | func (d *DefaultTracker) Spans() []Span {
61 | d.mu.Lock()
62 | defer d.mu.Unlock()
63 |
64 | var spans []Span
65 |
66 | for _, m := range d.metrics {
67 | spans = append(spans, Span{Name: m.Name, Duration: m.StopTime.Sub(m.StartTime)})
68 | }
69 |
70 | return spans
71 | }
72 |
73 | func FromContext(ctx context.Context) Tracker {
74 | tv := ctx.Value(trackVal{})
75 | if tv == nil {
76 | return &DefaultTracker{}
77 | }
78 |
79 | return tv.(Tracker)
80 | }
81 |
82 | func WithTracker(ctx context.Context, t Tracker) context.Context {
83 | return context.WithValue(ctx, trackVal{}, t)
84 | }
85 |
86 | func Track(ctx context.Context, name string) Metric {
87 | return FromContext(ctx).NewMetric(name).Start()
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/control/config.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | "fmt"
8 | "path/filepath"
9 |
10 | "github.com/hashicorp/go-multierror"
11 | "github.com/mitchellh/go-homedir"
12 | "k8s.io/client-go/rest"
13 | "k8s.io/client-go/tools/clientcmd"
14 | )
15 |
16 | // K8SConfig returns a *restclient.Config for initializing a K8S client.
17 | // This configuration first attempts to load a local kubeconfig if a
18 | // path is given. If that doesn't work, then in-cluster auth is used.
19 | func K8SConfig(path string) (*rest.Config, error) {
20 | // Get the configuration. This can come from multiple sources. We first
21 | // try kubeconfig it is set directly, then we fall back to in-cluster
22 | // auth. Finally, we try the default kubeconfig path.
23 | kubeconfig := path
24 | if kubeconfig == "" {
25 | // If kubeconfig is empty, let's first try the default directory.
26 | // This is must faster than trying in-cluster auth so we try this
27 | // first.
28 | dir, err := homedir.Dir()
29 | if err != nil {
30 | return nil, fmt.Errorf("error retrieving home directory: %s", err)
31 | }
32 | kubeconfig = filepath.Join(dir, ".kube", "config")
33 | }
34 |
35 | // First try to get the configuration from the kubeconfig value
36 | config, configErr := clientcmd.BuildConfigFromFlags("", kubeconfig)
37 | if configErr != nil {
38 | configErr = fmt.Errorf("error loading kubeconfig: %s", configErr)
39 |
40 | // kubeconfig failed, fall back and try in-cluster config. We do
41 | // this as the fallback since this makes network connections and
42 | // is much slower to fail.
43 | var err error
44 | config, err = rest.InClusterConfig()
45 | if err != nil {
46 | return nil, multierror.Append(configErr, fmt.Errorf(
47 | "error loading in-cluster config: %s", err))
48 | }
49 | }
50 |
51 | return config, nil
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/control/client_k8s.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 | "math/rand"
9 | "strings"
10 | "time"
11 |
12 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 | "k8s.io/client-go/kubernetes"
14 | )
15 |
16 | func (c *Client) ConnectToKubernetes() error {
17 | cfg, err := K8SConfig("")
18 | if err != nil {
19 | return err
20 | }
21 |
22 | cs, err := kubernetes.NewForConfig(cfg)
23 | if err != nil {
24 | return err
25 | }
26 |
27 | c.clientset = cs
28 |
29 | return nil
30 | }
31 |
32 | func (c *Client) checkImageTag(ctx context.Context, newTag string, delay bool) error {
33 | if c.clientset == nil || c.cfg.K8Deployment == "" {
34 | c.L.Debug("no kubernetes configuration set, ignoring tag", "tag", newTag)
35 | return nil
36 | }
37 |
38 | depapi := c.clientset.AppsV1().Deployments("default")
39 | dep, err := depapi.Get(ctx, c.cfg.K8Deployment, v1.GetOptions{})
40 | if err != nil {
41 | c.L.Error("error fetching deployment for image update", "error", err, "deployment", c.cfg.K8Deployment)
42 | return err
43 | }
44 |
45 | img := dep.Spec.Template.Spec.Containers[0].Image
46 |
47 | var repo string
48 |
49 | idx := strings.LastIndexByte(img, ':')
50 | if idx == -1 {
51 | repo = img
52 | } else {
53 | repo = img[:idx]
54 | if img[idx+1:] == newTag {
55 | c.L.Trace("deployment already at desired tag, skipping")
56 | return nil
57 | }
58 | }
59 |
60 | newImage := repo + ":" + newTag
61 |
62 | dep.Spec.Template.Spec.Containers[0].Image = newImage
63 |
64 | if delay {
65 | secs := rand.Int31n(10)
66 |
67 | c.L.Info("delaying before updating deployment", "seconds", secs)
68 | time.Sleep(time.Duration(secs) * time.Second)
69 | }
70 |
71 | c.L.Info("updating deployment", "image", newImage, "prev", img)
72 |
73 | _, err = depapi.Update(ctx, dep, v1.UpdateOptions{})
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/pb/account.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 | "errors"
9 |
10 | "github.com/mr-tron/base58"
11 | "golang.org/x/crypto/blake2b"
12 | )
13 |
14 | // Collapse the Account to an unambigious sequence
15 | func (a *Account) Key() []byte {
16 | var buf bytes.Buffer
17 | buf.WriteString(a.Namespace)
18 | buf.WriteRune('!')
19 | buf.Write(a.AccountId.Bytes())
20 |
21 | return buf.Bytes()
22 | }
23 |
24 | func (a *Account) StringKey() string {
25 | var buf bytes.Buffer
26 | buf.WriteString(a.Namespace)
27 | buf.WriteRune('!')
28 | buf.WriteString(a.AccountId.String())
29 |
30 | return buf.String()
31 | }
32 |
33 | func (a *Account) SpecString() string {
34 | return a.StringKey()
35 | }
36 |
37 | func (a *Account) String() string {
38 | return a.StringKey()
39 | }
40 |
41 | // Hash the data and present a stable hash key useful for use
42 | // in things like file and url paths.
43 | func (a *Account) HashKey() string {
44 | h, _ := blake2b.New256(nil)
45 | h.Write([]byte("hznaccount"))
46 | h.Write([]byte(a.Namespace))
47 | h.Write(a.AccountId.Bytes())
48 |
49 | return base58.Encode(h.Sum(nil))
50 | }
51 |
52 | var ErrInvalidAccount = errors.New("invalid account key")
53 |
54 | func AccountFromKey(k []byte) (*Account, error) {
55 | pos := bytes.IndexRune(k, '!')
56 | if pos == -1 {
57 | return nil, ErrInvalidAccount
58 | }
59 |
60 | namespace := k[:pos]
61 | id := k[pos+1:]
62 |
63 | return &Account{
64 | Namespace: string(namespace),
65 | AccountId: ULIDFromBytes(id),
66 | }, nil
67 | }
68 |
69 | func AccountFromStringKey(k []byte) (*Account, error) {
70 | pos := bytes.IndexRune(k, '!')
71 | if pos == -1 {
72 | return nil, ErrInvalidAccount
73 | }
74 |
75 | namespace := k[:pos]
76 | id := k[pos+1:]
77 |
78 | return &Account{
79 | Namespace: string(namespace),
80 | AccountId: ULIDFromBytes(id),
81 | }, nil
82 | }
83 |
--------------------------------------------------------------------------------
/kubernetes/hub.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | apiVersion: apps/v1
5 | kind: Deployment
6 | metadata:
7 | name: hub
8 | labels:
9 | app: hub
10 | spec:
11 | replicas: 1
12 | selector:
13 | matchLabels:
14 | app: hub
15 | template:
16 | metadata:
17 | labels:
18 | app: hub
19 | annotations:
20 | prometheus.io/scrape: "true"
21 | prometheus.io/path: /metrics
22 | prometheus.io/port: "17001"
23 |
24 | spec:
25 | containers:
26 | - name: hub
27 | image: 074735953993.dkr.ecr.us-west-2.amazonaws.com/horizon:latest
28 | args:
29 | - hub
30 | env:
31 | - name: AWS_REGION
32 | value: us-west-2
33 |
34 | - name: CONTROL_ADDR
35 | valueFrom:
36 | configMapKeyRef:
37 | name: hub
38 | key: control-addr
39 |
40 | - name: STABLE_ID
41 | valueFrom:
42 | configMapKeyRef:
43 | name: hub
44 | key: stable-id
45 |
46 | - name: TOKEN
47 | valueFrom:
48 | secretKeyRef:
49 | name: hub
50 | key: token
51 |
52 | - name: HTTP_PORT
53 | value: "80"
54 |
55 | - name: DEBUG
56 | valueFrom:
57 | configMapKeyRef:
58 | name: hub
59 | key: debug
60 |
61 | - name: PORT
62 | value: "443"
63 |
64 | ports:
65 | - name: hzn
66 | containerPort: 443
67 | - name: monitoring
68 | containerPort: 17001
69 | - name: http
70 | containerPort: 80
71 |
72 | livenessProbe:
73 | httpGet:
74 | path: /healthz
75 | port: monitoring
76 | initialDelaySeconds: 3
77 | periodSeconds: 3
78 |
79 |
--------------------------------------------------------------------------------
/pkg/pb/label_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package pb
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestLabelSet(t *testing.T) {
14 | t.Run("can match a subset", func(t *testing.T) {
15 | ls := ParseLabelSet("service=www,env=prod,instance=aabbcc")
16 |
17 | target := ParseLabelSet("service=www,env=prod")
18 |
19 | require.True(t, target.Matches(ls))
20 | require.False(t, ls.Matches(target))
21 | })
22 |
23 | t.Run("uses case folding on match", func(t *testing.T) {
24 | ls := ParseLabelSet("service=www,env=prod,instance=aabbcc")
25 |
26 | target := ParseLabelSet("service=www,env=prod")
27 |
28 | target.Labels[1].Name = "ServICE"
29 | target.Labels[1].Value = "WWW"
30 |
31 | require.True(t, target.Matches(ls))
32 | require.False(t, ls.Matches(target))
33 | })
34 |
35 | t.Run("is stabley sorted", func(t *testing.T) {
36 | ls := ParseLabelSet("service=emp,env=test")
37 | assert.Equal(t, "env", ls.Labels[0].Name)
38 |
39 | ls.Finalize()
40 |
41 | assert.Equal(t, "env", ls.Labels[0].Name)
42 |
43 | ls.Finalize()
44 |
45 | assert.Equal(t, "env", ls.Labels[0].Name)
46 | })
47 |
48 | t.Run("sorts by name then value", func(t *testing.T) {
49 | ls := ParseLabelSet("service=emp,env=test")
50 | assert.Equal(t, "env", ls.Labels[0].Name)
51 |
52 | ls = ParseLabelSet("a=x,b=c")
53 | assert.Equal(t, "a", ls.Labels[0].Name)
54 |
55 | ls = ParseLabelSet("service=foo,service=emp")
56 | assert.Equal(t, "foo", ls.Labels[1].Value)
57 | })
58 |
59 | t.Run("lowercases on parsing", func(t *testing.T) {
60 | ls := ParseLabelSet("service=emp,ENV=test")
61 | assert.Equal(t, "env", ls.Labels[0].Name)
62 |
63 | ls.Finalize()
64 |
65 | assert.Equal(t, "env", ls.Labels[0].Name)
66 |
67 | ls.Finalize()
68 |
69 | assert.Equal(t, "env", ls.Labels[0].Name)
70 | })
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/agent/http.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "io"
10 | "net/http"
11 | "net/url"
12 | "strings"
13 |
14 | "github.com/hashicorp/go-hclog"
15 | "github.com/hashicorp/horizon/pkg/pb"
16 | )
17 |
18 | type httpHandler struct {
19 | url string
20 | }
21 |
22 | func HTTPHandler(url string) ServiceHandler {
23 | if !strings.HasPrefix(url, "http://") {
24 | url = "http://" + url
25 | }
26 |
27 | return &httpHandler{url}
28 | }
29 |
30 | func (h *httpHandler) HandleRequest(ctx context.Context, L hclog.Logger, sctx ServiceContext) error {
31 | proto := sctx.ProtocolId()
32 |
33 | if !(proto == "" || proto == "http") {
34 | return fmt.Errorf("unknown protocol: %s", proto)
35 | }
36 |
37 | var req pb.Request
38 |
39 | _, err := sctx.ReadMarshal(&req)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | L.Info("request started", "method", req.Method, "path", req.Path)
45 |
46 | hreq, err := http.NewRequestWithContext(ctx, req.Method, h.url+req.Path, sctx.BodyReader())
47 | if err != nil {
48 | return err
49 | }
50 |
51 | hreq.Host = req.Host
52 | hreq.URL.RawQuery = req.Query
53 | hreq.URL.Fragment = req.Fragment
54 | if req.Auth != nil {
55 | hreq.URL.User = url.UserPassword(req.Auth.User, req.Auth.Password)
56 | }
57 | for _, h := range req.Headers {
58 | for _, v := range h.Value {
59 | hreq.Header.Add(h.Name, v)
60 | }
61 | }
62 |
63 | hresp, err := http.DefaultClient.Do(hreq)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | defer hresp.Body.Close()
69 |
70 | var resp pb.Response
71 | resp.Code = int32(hresp.StatusCode)
72 |
73 | for k, v := range hresp.Header {
74 | resp.Headers = append(resp.Headers, &pb.Header{
75 | Name: k,
76 | Value: v,
77 | })
78 | }
79 |
80 | err = sctx.WriteMarshal(1, &resp)
81 | if err != nil {
82 | return err
83 | }
84 |
85 | w := sctx.Writer()
86 | defer w.Close()
87 |
88 | n, _ := io.Copy(w, hresp.Body)
89 |
90 | L.Info("request ended", "size", n)
91 |
92 | return nil
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/control/activity_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | "context"
8 | "testing"
9 | "time"
10 |
11 | "github.com/hashicorp/horizon/internal/testsql"
12 | "github.com/hashicorp/horizon/pkg/dbx"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | func TestActivity(t *testing.T) {
18 | const testDbName = "hzn_control"
19 |
20 | t.Run("reader can return new log events", func(t *testing.T) {
21 | db := testsql.TestPostgresDB(t, testDbName)
22 | defer db.Close()
23 |
24 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
25 | defer cancel()
26 |
27 | ar, err := NewActivityReader(ctx, "postgres",
28 | testsql.TestPostgresDBString(t, testDbName))
29 | require.NoError(t, err)
30 |
31 | defer ar.Close()
32 |
33 | time.Sleep(time.Second)
34 |
35 | ai, err := NewActivityInjector(db)
36 | require.NoError(t, err)
37 |
38 | err = ai.Inject(ctx, []byte(`"this is an event"`))
39 | require.NoError(t, err)
40 |
41 | select {
42 | case <-ctx.Done():
43 | require.NoError(t, ctx.Err())
44 | case entries := <-ar.C:
45 | assert.Equal(t, []byte(`"this is an event"`), entries[0].Event)
46 | }
47 |
48 | err = ai.Inject(ctx, []byte(`"this is a second event"`))
49 | require.NoError(t, err)
50 |
51 | select {
52 | case <-ctx.Done():
53 | require.NoError(t, ctx.Err())
54 | case entries := <-ar.C:
55 | assert.Equal(t, []byte(`"this is a second event"`), entries[0].Event)
56 | }
57 | })
58 |
59 | t.Run("prunes old logs", func(t *testing.T) {
60 | db := testsql.TestPostgresDB(t, testDbName)
61 | defer db.Close()
62 |
63 | var ae ActivityLog
64 | ae.CreatedAt = time.Now().Add(-6 * time.Hour)
65 | ae.Event = []byte(`1`)
66 |
67 | err := dbx.Check(db.Create(&ae))
68 | require.NoError(t, err)
69 |
70 | var lc LogCleaner
71 | lc.DB = db
72 | err = lc.CleanupActivityLog(nil, "cleanup-activity-log", nil)
73 | require.NoError(t, err)
74 |
75 | var ae2 ActivityLog
76 | err = dbx.Check(db.First(&ae2))
77 | require.Error(t, err)
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hashicorp/horizon
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/armon/go-metrics v0.3.3
7 | github.com/aws/aws-sdk-go v1.25.41
8 | github.com/caddyserver/certmagic v0.10.3
9 | github.com/davecgh/go-spew v1.1.1
10 | github.com/frankban/quicktest v1.4.1 // indirect
11 | github.com/go-acme/lego/v3 v3.5.0
12 | github.com/go-redis/redis/v8 v8.11.3
13 | github.com/gogo/protobuf v1.3.1
14 | github.com/golang-migrate/migrate/v4 v4.10.0
15 | github.com/golang/protobuf v1.5.2
16 | github.com/hashicorp/consul/api v1.7.0
17 | github.com/hashicorp/go-cleanhttp v0.5.1
18 | github.com/hashicorp/go-hclog v0.13.0
19 | github.com/hashicorp/go-immutable-radix v1.2.0 // indirect
20 | github.com/hashicorp/go-multierror v1.1.0
21 | github.com/hashicorp/go-uuid v1.0.2 // indirect
22 | github.com/hashicorp/golang-lru v0.5.3
23 | github.com/hashicorp/vault/api v1.0.5-0.20190909201928-35325e2c3262
24 | github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864
25 | github.com/imdario/mergo v0.3.8 // indirect
26 | github.com/jinzhu/gorm v1.9.12
27 | github.com/klauspost/compress v1.10.10
28 | github.com/lib/pq v1.3.0
29 | github.com/mitchellh/cli v1.1.0
30 | github.com/mitchellh/go-homedir v1.1.0
31 | github.com/mitchellh/go-server-timing v1.0.0
32 | github.com/mitchellh/go-testing-interface v1.0.0
33 | github.com/mitchellh/mapstructure v1.1.2
34 | github.com/mr-tron/base58 v1.1.3
35 | github.com/oklog/ulid v1.3.1
36 | github.com/oschwald/geoip2-golang v1.4.0
37 | github.com/pierrec/lz4 v2.2.6+incompatible
38 | github.com/pierrec/lz4/v3 v3.3.2
39 | github.com/pkg/errors v0.9.1
40 | github.com/prometheus/client_golang v1.4.0
41 | github.com/spf13/pflag v1.0.5
42 | github.com/stretchr/testify v1.5.1
43 | go.etcd.io/bbolt v1.3.3
44 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
45 | golang.org/x/net v0.0.0-20210825183410-e898025ed96a
46 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
47 | google.golang.org/genproto v0.0.0-20200416231807-8751e049a2a0 // indirect
48 | google.golang.org/grpc v1.28.1
49 | gopkg.in/square/go-jose.v2 v2.4.1 // indirect
50 | gortc.io/stun v1.22.2
51 | k8s.io/apimachinery v0.18.0
52 | k8s.io/client-go v0.18.0
53 | )
54 |
--------------------------------------------------------------------------------
/pkg/utils/cert.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package utils
5 |
6 | import (
7 | "bytes"
8 | "crypto/ed25519"
9 | "crypto/rand"
10 | "crypto/tls"
11 | "crypto/x509"
12 | "crypto/x509/pkix"
13 | "encoding/pem"
14 | "math/big"
15 | "net"
16 | "time"
17 | )
18 |
19 | func SelfSignedCert() ([]byte, []byte, error) {
20 | tlspub, tlspriv, err := ed25519.GenerateKey(rand.Reader)
21 | if err != nil {
22 | return nil, nil, err
23 | }
24 |
25 | notBefore := time.Now()
26 |
27 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
28 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
29 | if err != nil {
30 | return nil, nil, err
31 | }
32 |
33 | template := x509.Certificate{
34 | SerialNumber: serialNumber,
35 | Subject: pkix.Name{
36 | Organization: []string{"Acme Co"},
37 | },
38 | NotBefore: time.Now(),
39 | NotAfter: notBefore.Add(5 * time.Minute),
40 |
41 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
42 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
43 | BasicConstraintsValid: true,
44 | DNSNames: []string{"hub.test"},
45 | IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
46 | IsCA: true,
47 | }
48 |
49 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, tlspub, tlspriv)
50 |
51 | var certBuf, keyBuf bytes.Buffer
52 |
53 | pem.Encode(&certBuf, &pem.Block{
54 | Type: "CERTIFICATE",
55 | Bytes: derBytes,
56 | })
57 |
58 | keybytes, err := x509.MarshalPKCS8PrivateKey(tlspriv)
59 | if err != nil {
60 | return nil, nil, err
61 | }
62 |
63 | pem.Encode(&keyBuf, &pem.Block{
64 | Type: "PRIVATE KEY",
65 | Bytes: keybytes,
66 | })
67 |
68 | return certBuf.Bytes(), keyBuf.Bytes(), nil
69 | }
70 |
71 | func TrustedTLSConfig(cert []byte) (*tls.Config, error) {
72 | parsedHubCert, err := x509.ParseCertificate(cert)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | var tlscfg tls.Config
78 |
79 | tlscfg.RootCAs = x509.NewCertPool()
80 | tlscfg.RootCAs.AddCert(parsedHubCert)
81 |
82 | return &tlscfg, nil
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/pb/wire.proto:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | syntax = "proto3";
5 |
6 | import "timestamp.proto";
7 | import "kv.proto";
8 | import "ulid.proto";
9 | import "account.proto";
10 | import "label.proto";
11 |
12 | package pb;
13 |
14 | message Labels {
15 | // These labels are used to identify this specific service instance
16 | repeated string label = 1;
17 | }
18 |
19 | message ServiceInfo {
20 | // A short identifier for this service instance
21 | ULID service_id = 1;
22 |
23 | // A type identifier for the kind of service this instance is
24 | string type = 2;
25 |
26 | // These labels are used to identify this specific service instance
27 | LabelSet labels = 3;
28 |
29 | repeated KVPair metadata = 4;
30 | }
31 |
32 | message Preamble {
33 | string session_id = 1;
34 | string token = 2;
35 |
36 | // These are labels that identify the agent. IE location, etc.
37 | repeated string labels = 3;
38 | repeated ServiceInfo services = 4;
39 |
40 | string compression = 5;
41 | }
42 |
43 | message Confirmation {
44 | Timestamp time = 1;
45 | string status = 2;
46 | string compression = 3;
47 | }
48 |
49 | message Header {
50 | string name = 1;
51 | repeated string value = 2;
52 | }
53 |
54 | message Auth {
55 | string user = 1;
56 | string password = 2;
57 | }
58 |
59 | message ConnectRequest {
60 | LabelSet target = 1;
61 | string type = 2;
62 | Account pivot_account = 3;
63 | string protocol_id = 4;
64 | bytes source_addr = 5;
65 | }
66 |
67 | message ConnectAck {
68 | ULID service_id = 1;
69 | }
70 |
71 | message SessionIdentification {
72 | ULID service_id = 1;
73 | string protocol_id = 2;
74 | }
75 |
76 | message Request {
77 | enum Type {
78 | HTTP = 0;
79 | WEBSOCKET = 1;
80 | TCP = 2;
81 | UDP = 3;
82 | RPC = 4;
83 | AGENT_CONNECT = 5;
84 | }
85 |
86 | Type type = 1;
87 | string method = 2;
88 | string path = 3;
89 | string query = 4;
90 | string fragment = 5;
91 | Auth auth = 6;
92 | repeated Header headers = 7;
93 | string remote_addr = 8;
94 | string host = 9;
95 | bytes agentId = 10;
96 | string target_service = 11;
97 | Account pivot_account = 12;
98 | }
99 |
100 | message Response {
101 | string error = 1;
102 | int32 code = 2;
103 | repeated Header headers = 3;
104 | }
105 |
--------------------------------------------------------------------------------
/pkg/control/server_edge.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 |
9 | "github.com/hashicorp/horizon/pkg/dbx"
10 | "github.com/hashicorp/horizon/pkg/pb"
11 | )
12 |
13 | func (s *Server) LookupEndpoints(ctx context.Context, req *pb.LookupEndpointsRequest) (*pb.LookupEndpointsResponse, error) {
14 | _, err := s.checkFromHub(ctx, "lookup-endpoints")
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return s.queryDBServices(ctx, req)
20 | }
21 |
22 | func (s *Server) queryDBServices(ctx context.Context, req *pb.LookupEndpointsRequest) (*pb.LookupEndpointsResponse, error) {
23 | var services []*Service
24 | err := dbx.Check(
25 | s.db.Where("account_id = ? AND labels @> ?", req.Account.Key(), req.Labels.AsStringArray()).Find(&services))
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | var resp pb.LookupEndpointsResponse
31 |
32 | for _, serv := range services {
33 | var ls pb.LabelSet
34 |
35 | err = ls.Scan(serv.Labels)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | resp.Routes = append(resp.Routes, &pb.ServiceRoute{
41 | Hub: pb.ULIDFromBytes(serv.HubId),
42 | Id: pb.ULIDFromBytes(serv.ServiceId),
43 | Type: serv.Type,
44 | Labels: &ls,
45 | })
46 | }
47 |
48 | return &resp, nil
49 | }
50 |
51 | func (s *Server) ResolveLabelLink(ctx context.Context, req *pb.ResolveLabelLinkRequest) (*pb.ResolveLabelLinkResponse, error) {
52 | _, err := s.checkFromHub(ctx, "resolve-label-link")
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return s.queryDBLabelLinks(ctx, req)
58 | }
59 |
60 | func (s *Server) queryDBLabelLinks(ctx context.Context, req *pb.ResolveLabelLinkRequest) (*pb.ResolveLabelLinkResponse, error) {
61 | var ll LabelLink
62 |
63 | err := dbx.Check(
64 | s.db.Where("labels = ?", FlattenLabels(req.Labels)).Preload("Account").First(&ll))
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | var resp pb.ResolveLabelLinkResponse
70 |
71 | target := ExplodeLabels(ll.Target)
72 |
73 | account, err := pb.AccountFromKey(ll.AccountID)
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | var pblimit pb.Account_Limits
79 | ll.Account.Data.Get("limits", &pblimit)
80 |
81 | resp.Labels = target
82 | resp.Account = account
83 | resp.Limits = &pblimit
84 |
85 | return &resp, nil
86 | }
87 |
88 | var _ pb.EdgeServicesServer = (*Server)(nil)
89 |
--------------------------------------------------------------------------------
/pkg/pb/edge.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: edge.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *LookupEndpointsRequest) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *LookupEndpointsRequest) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *LookupEndpointsResponse) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *LookupEndpointsResponse) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
48 | // MarshalJSON implements json.Marshaler
49 | func (msg *ResolveLabelLinkRequest) MarshalJSON() ([]byte, error) {
50 | var buf bytes.Buffer
51 | err := (&jsonpb.Marshaler{
52 | EnumsAsInts: false,
53 | EmitDefaults: false,
54 | OrigName: false,
55 | }).Marshal(&buf, msg)
56 | return buf.Bytes(), err
57 | }
58 |
59 | // UnmarshalJSON implements json.Unmarshaler
60 | func (msg *ResolveLabelLinkRequest) UnmarshalJSON(b []byte) error {
61 | return (&jsonpb.Unmarshaler{
62 | AllowUnknownFields: false,
63 | }).Unmarshal(bytes.NewReader(b), msg)
64 | }
65 |
66 | // MarshalJSON implements json.Marshaler
67 | func (msg *ResolveLabelLinkResponse) MarshalJSON() ([]byte, error) {
68 | var buf bytes.Buffer
69 | err := (&jsonpb.Marshaler{
70 | EnumsAsInts: false,
71 | EmitDefaults: false,
72 | OrigName: false,
73 | }).Marshal(&buf, msg)
74 | return buf.Bytes(), err
75 | }
76 |
77 | // UnmarshalJSON implements json.Unmarshaler
78 | func (msg *ResolveLabelLinkResponse) UnmarshalJSON(b []byte) error {
79 | return (&jsonpb.Unmarshaler{
80 | AllowUnknownFields: false,
81 | }).Unmarshal(bytes.NewReader(b), msg)
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/workq/periodic_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "testing"
8 | "time"
9 |
10 | "github.com/hashicorp/go-hclog"
11 | "github.com/hashicorp/horizon/internal/testsql"
12 | "github.com/hashicorp/horizon/pkg/dbx"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | func TestPeriodic(t *testing.T) {
18 | L := hclog.L()
19 |
20 | t.Run("creates jobs from periodic jobs", func(t *testing.T) {
21 | db := testsql.TestPostgresDB(t, "periodic")
22 | defer db.Close()
23 |
24 | var pjob PeriodicJob
25 |
26 | pjob.NextRun = time.Now()
27 | pjob.Queue = "a"
28 | pjob.Period = "30m"
29 | pjob.JobType = "test"
30 | pjob.Payload = []byte("1")
31 |
32 | err := dbx.Check(db.Create(&pjob))
33 | require.NoError(t, err)
34 |
35 | w := NewWorker(L, db, []string{"a"})
36 |
37 | err = w.CheckPeriodic()
38 | require.NoError(t, err)
39 |
40 | var job Job
41 |
42 | err = dbx.Check(db.First(&job))
43 | require.NoError(t, err)
44 |
45 | assert.Equal(t, pjob.Queue, job.Queue)
46 | assert.Equal(t, pjob.Payload, job.Payload)
47 |
48 | var pjob2 PeriodicJob
49 |
50 | err = dbx.Check(db.First(&pjob2))
51 |
52 | assert.NotEqual(t, pjob.NextRun, pjob2.NextRun)
53 |
54 | err = w.CheckPeriodic()
55 | require.NoError(t, err)
56 |
57 | })
58 |
59 | t.Run("creates or updates periodic jobs", func(t *testing.T) {
60 | db := testsql.TestPostgresDB(t, "periodic")
61 | defer db.Close()
62 |
63 | var i Injector
64 | i.db = db
65 |
66 | err := i.AddPeriodicJob("foo", "a", "test", "aabbcc", time.Hour)
67 | require.NoError(t, err)
68 |
69 | var pjob PeriodicJob
70 |
71 | err = dbx.Check(db.First(&pjob))
72 | require.NoError(t, err)
73 |
74 | err = i.AddPeriodicJob("foo", "a", "test", "aabbccdd", time.Minute)
75 | require.NoError(t, err)
76 |
77 | var pjob2 PeriodicJob
78 |
79 | err = dbx.Check(db.Last(&pjob2))
80 | require.NoError(t, err)
81 |
82 | assert.Equal(t, pjob.Id, pjob2.Id)
83 |
84 | assert.Equal(t, pjob2.Payload, []byte(`"aabbccdd"`))
85 | assert.True(t, pjob2.NextRun.Before(pjob.NextRun))
86 |
87 | err = i.AddPeriodicJob("foo", "a", "test", "aabbccdd", time.Hour)
88 | require.NoError(t, err)
89 |
90 | var pjob3 PeriodicJob
91 |
92 | err = dbx.Check(db.Last(&pjob3))
93 | require.NoError(t, err)
94 |
95 | assert.Equal(t, pjob.Id, pjob2.Id)
96 | assert.True(t, pjob2.NextRun.Equal(pjob3.NextRun))
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/internal/httpassets/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 | HashiCorp Waypoint URL Service
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 | Waypoint is a tool built by
24 | HashiCorp for building, deploying
25 | and releasing applications.
26 |
27 |
28 |
29 | Applications deployed with Waypoint receive a public
30 | waypoint.run URL like this one, for viewing and sharing
31 | deployed applications.
32 |
33 |
34 |
35 |
36 |
37 | Waypoint allows developers to define their application build, deploy,
38 | and release lifecycle as code, reducing the time to deliver
39 | deployments through a consistent and repeatable workflow.
40 |
41 |
42 | Visit waypointproject.io
45 | View documentation
50 |
51 |
52 |
53 |
54 | If you have questions about Waypoint, please contact us at the
55 | HashiCorp Discussion Forums .
57 |
58 |
59 | The HashiCorp-managed instance of this service at https://waypoint.run
60 | is subject to Waypoint’s
61 | Terms of Use ,
62 | Security , and
63 | Privacy policies.
64 |
65 | © 2020 HashiCorp, Inc.
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgsPath ? }:
2 |
3 | let
4 | # First we setup our overlays. These are overrides of the official nix packages.
5 | # We do this to pin the versions we want to use of the software that is in
6 | # the official nixpkgs repo.
7 | pkgs = import pkgsPath {
8 | overlays = [(self: super: {
9 |
10 | go = super.go.overrideAttrs ( old: rec {
11 | version = "1.14.4";
12 | src = super.fetchurl {
13 | url = "https://dl.google.com/go/go${version}.src.tar.gz";
14 | sha256 = "1105qk2l4kfy1ki9n9gh8j4gfqrfgfwapa1fp38hih9aphxsy4bh";
15 | };
16 | });
17 |
18 | go-protobuf = super.go-protobuf.overrideAttrs ( old: rec {
19 | version = "1.3.5";
20 | src = super.fetchFromGitHub {
21 | owner = "golang";
22 | repo = "protobuf";
23 | rev = "v${version}";
24 | sha256 = "1gkd1942vk9n8kfzdwy1iil6wgvlwjq7a3y5jc49ck4lz9rhmgkq";
25 | };
26 |
27 | modSha256 = "0jjjj9z1dhilhpc8pq4154czrb79z9cm044jvn75kxcjv6v5l2m5";
28 | });
29 |
30 | })];
31 | };
32 | in with pkgs; let
33 | go-protobuf-gogo = buildGoModule rec {
34 | pname = "go-protobuf-gogo";
35 | version = "1.3.1";
36 |
37 | src = fetchFromGitHub {
38 | owner = "gogo";
39 | repo = "protobuf";
40 | rev = "v1.3.1";
41 | sha256 = "0x77x64sxjgfhmbijqfzmj8h4ar25l2w97h01q3cqs1wk7zfnkhp";
42 | };
43 |
44 | modSha256 = "0vkpqdd4x97cl3dm79mh1vic1ir4i20wv9q52sn13vr0b3kja0qy";
45 |
46 | subPackages = [ "protoc-gen-gogoslick" ];
47 | };
48 |
49 | go-bindata = buildGoModule rec {
50 | pname = "go-bindata";
51 | version = "3.1.3";
52 |
53 | src = fetchFromGitHub {
54 | owner = "go-bindata";
55 | repo = "go-bindata";
56 | rev = "v3.1.3";
57 | sha256 = "0ql9i1bjl6bkznypjqyj4jhs29rvz48kdc32qnh53l1aiyk77gx2";
58 | };
59 |
60 | modSha256 = "0wj5llknx2wcrm9cfpvw9cbnngdg4x2k3ha3d53q9clc5dzagy21";
61 |
62 | subPackages = [ "go-bindata" ];
63 | };
64 | in pkgs.mkShell rec {
65 | name = "horizon";
66 |
67 | # The packages in the `buildInputs` list will be added to the PATH in our shell
68 | buildInputs = [
69 | pkgs.go
70 | go-bindata
71 | pkgs.go-protobuf
72 | pkgs.protobuf3_11
73 | pkgs.postgresql_12
74 | go-protobuf-gogo
75 | ];
76 |
77 | # Extra env vars
78 | PGHOST = "localhost";
79 | PGPORT = "5432";
80 | PGDATABASE = "noop";
81 | PGUSER = "postgres";
82 | PGPASSWORD = "postgres";
83 | DATABASE_URL = "postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/${PGDATABASE}?sslmode=disable";
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/workq/registry.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package workq
5 |
6 | import (
7 | "context"
8 | "encoding/json"
9 | "fmt"
10 | "reflect"
11 | "sync"
12 |
13 | "github.com/hashicorp/go-hclog"
14 | "github.com/pkg/errors"
15 | )
16 |
17 | type Handler interface {
18 | PerformJob(jobType string, data []byte) error
19 | }
20 |
21 | type registeredHandler struct {
22 | argType reflect.Type
23 | f reflect.Value
24 | }
25 |
26 | type Registry struct {
27 | mu sync.RWMutex
28 | types map[string]registeredHandler
29 | }
30 |
31 | func (r *Registry) PrintHandlers(L hclog.Logger) {
32 | r.mu.Lock()
33 | defer r.mu.Unlock()
34 |
35 | L.Info("registry handlers", "total", len(r.types))
36 |
37 | for jt := range r.types {
38 | L.Info("registered handler for job type", "type", jt)
39 | }
40 | }
41 |
42 | func (r *Registry) Register(jobType string, h interface{}) {
43 | r.mu.Lock()
44 | defer r.mu.Unlock()
45 |
46 | if r.types == nil {
47 | r.types = make(map[string]registeredHandler)
48 | }
49 |
50 | v := reflect.ValueOf(h)
51 | if v.Kind() != reflect.Func {
52 | panic("register must be passed a func")
53 | }
54 |
55 | ft := v.Type()
56 |
57 | if ft.NumIn() != 3 {
58 | panic("register func takes 3 arguments")
59 | }
60 |
61 | ct := reflect.TypeOf((*context.Context)(nil)).Elem()
62 |
63 | if ft.In(0) != ct {
64 | panic(fmt.Sprintf("register func first arg must be a context, was %s", ct))
65 | }
66 |
67 | if ft.In(1) != reflect.TypeOf("") {
68 | panic("register func second arg must be a string")
69 | }
70 |
71 | var err error
72 |
73 | if ft.Out(0) != reflect.TypeOf(&err).Elem() {
74 | panic("register out must return an error")
75 | }
76 |
77 | argt := ft.In(2)
78 |
79 | r.types[jobType] = registeredHandler{
80 | argType: argt,
81 | f: v,
82 | }
83 | }
84 |
85 | func (r *Registry) Handle(ctx context.Context, job *Job) error {
86 | r.mu.RLock()
87 |
88 | rh, ok := r.types[job.JobType]
89 |
90 | r.mu.RUnlock()
91 |
92 | if !ok {
93 | return nil
94 | }
95 |
96 | arg := reflect.New(rh.argType.Elem())
97 |
98 | err := json.Unmarshal(job.Payload, arg.Interface())
99 | if err != nil {
100 | return errors.Wrapf(err, "wrong json for job type: %s", job.JobType)
101 | }
102 |
103 | out := rh.f.Call([]reflect.Value{
104 | reflect.ValueOf(ctx), reflect.ValueOf(job.JobType), arg,
105 | })
106 |
107 | v := out[0]
108 |
109 | if v.IsNil() {
110 | return nil
111 | }
112 |
113 | return v.Interface().(error)
114 | }
115 |
116 | func (r *Registry) Size() int {
117 | r.mu.RLock()
118 | defer r.mu.RUnlock()
119 | return len(r.types)
120 | }
121 |
--------------------------------------------------------------------------------
/internal/sqljson/data.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package sqljson
5 |
6 | import (
7 | "database/sql/driver"
8 | "encoding/json"
9 | "fmt"
10 | )
11 |
12 | // Data is a type that can be used as an attribute type in a GORM model to
13 | // support storing arbitrary data in a record. The Data struct handles
14 | // initialization of the underlying map internally.
15 | //
16 | // Data implements (database/sql/driver).Valuer and (database/sql).Scanner
17 | // in order to be serializable into a SQL database. The encoding of the stored
18 | // value is JSON so it can be stored in JSON column types.
19 | //
20 | // To support arbitrary values be encoded, Data uses json.RawMessage which
21 | // allows the decoding into concrete types to be deferred until they are
22 | // accessed. Use the Get and Set methods to access values.
23 | type Data map[string]json.RawMessage
24 |
25 | // Scan implements sql.Scanner and creates a Data instance with data from a
26 | // database query result.
27 | func (d *Data) Scan(src interface{}) error {
28 | b, ok := src.([]byte)
29 | if !ok {
30 | return fmt.Errorf(
31 | "invalid database value of type %T, type assertion to []byte failed",
32 | src,
33 | )
34 | }
35 |
36 | return json.Unmarshal(b, &d)
37 | }
38 |
39 | // Value implements driver.Valuer and creates a value that can be stored in a
40 | // database field from the Data instance.
41 | func (d Data) Value() (driver.Value, error) {
42 | return json.Marshal(d)
43 | }
44 |
45 | // Get extracts the data value identified by the given key and decodes it
46 | // into the passed out value. The type of the out value should be the same that
47 | // was passed to Set when writing the value.
48 | //
49 | // The first return value indicates whether there is a data value with the
50 | // given key. An error is returned as the second return value if decoding fails.
51 | func (d Data) Get(key string, out interface{}) (bool, error) {
52 | v, ok := d[key]
53 | if !ok {
54 | return false, nil
55 | }
56 |
57 | return true, json.Unmarshal(v, out)
58 | }
59 |
60 | // Set stores the given key/value pair in the Data instance. If there
61 | // already is a value with this key, it is overwritten.
62 | // The value can be of any type that is JSON serializable and deserializable.
63 | //
64 | // Note that if the value includes dynamic types (e.g. a struct with an
65 | // attribute with an interface type), the value should implement
66 | // json.Unmarshaler.
67 | //
68 | // An error is returned if encoding the value fails.
69 | func (d *Data) Set(key string, value interface{}) error {
70 | j, err := json.Marshal(value)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | if *d == nil {
76 | *d = Data{}
77 | }
78 |
79 | (*d)[key] = j
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/control/flow_top.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 | "sort"
9 | "time"
10 |
11 | lru "github.com/hashicorp/golang-lru"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | "google.golang.org/grpc/metadata"
14 | )
15 |
16 | type FlowTop struct {
17 | entries *lru.ARCCache
18 | }
19 |
20 | const DefaultFlowTopSize = 100
21 |
22 | func NewFlowTop(count int) (*FlowTop, error) {
23 | ent, err := lru.NewARC(count)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | return &FlowTop{
29 | entries: ent,
30 | }, nil
31 | }
32 |
33 | type FlowTopEntry struct {
34 | agg *pb.FlowStream
35 | updated time.Time
36 | }
37 |
38 | func (f *FlowTop) Add(rec *pb.FlowStream) {
39 | key := rec.FlowId.String()
40 | v, ok := f.entries.Get(key)
41 | if !ok {
42 | entry := &FlowTopEntry{agg: rec, updated: time.Now()}
43 | f.entries.Add(key, entry)
44 | } else {
45 | entry := v.(*FlowTopEntry)
46 |
47 | entry.updated = time.Now()
48 | entry.agg.EndedAt = rec.EndedAt
49 | entry.agg.NumMessages += rec.NumMessages
50 | entry.agg.NumBytes += rec.NumBytes
51 | }
52 | }
53 |
54 | func (f *FlowTop) Export() ([]*FlowTopEntry, error) {
55 | entries := make([]*FlowTopEntry, 0, f.entries.Len())
56 |
57 | keys := f.entries.Keys()
58 |
59 | for _, k := range keys {
60 | if v, ok := f.entries.Peek(k); ok {
61 | entries = append(entries, v.(*FlowTopEntry))
62 | }
63 | }
64 |
65 | sort.Slice(entries, func(i, j int) bool {
66 | a := entries[i]
67 | b := entries[j]
68 |
69 | if a.agg.EndedAt == nil {
70 | if b.agg.EndedAt == nil {
71 | return a.updated.Before(b.updated)
72 | } else {
73 | return false
74 | }
75 | } else {
76 | if b.agg.EndedAt == nil {
77 | return true
78 | }
79 |
80 | return a.agg.EndedAt.Time().Before(b.agg.EndedAt.Time())
81 | }
82 | })
83 |
84 | return entries, nil
85 | }
86 |
87 | func (s *Server) checkOpsAllowed(ctx context.Context) bool {
88 | md, ok := metadata.FromIncomingContext(ctx)
89 | if !ok {
90 | return false
91 | }
92 |
93 | auth := md["authorization"]
94 |
95 | if len(auth) < 1 {
96 | return false
97 | }
98 |
99 | return auth[0] == s.opsToken
100 | }
101 |
102 | func (s *Server) CurrentFlowTop(ctx context.Context, req *pb.FlowTopRequest) (*pb.FlowTopSnapshot, error) {
103 | entries, err := s.flowTop.Export()
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | if req.MaxRecords > 0 && len(entries) > int(req.MaxRecords) {
109 | entries = entries[:req.MaxRecords]
110 | }
111 |
112 | var snap pb.FlowTopSnapshot
113 |
114 | for _, e := range entries {
115 | snap.Records = append(snap.Records, e.agg)
116 | }
117 |
118 | return &snap, nil
119 | }
120 |
--------------------------------------------------------------------------------
/pkg/netloc/best_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package netloc
5 |
6 | import (
7 | "testing"
8 | "time"
9 |
10 | "github.com/hashicorp/horizon/pkg/pb"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestFindBest(t *testing.T) {
16 | t.Run("find locations that match exactly", func(t *testing.T) {
17 | lloc := []*pb.NetworkLocation{
18 | {
19 | Addresses: []string{"1.1.1.1"},
20 | Labels: pb.ParseLabelSet("dc=x"),
21 | },
22 | }
23 |
24 | rloc := []*pb.NetworkLocation{
25 | {
26 | Addresses: []string{"2.2.2.2"},
27 | Labels: pb.ParseLabelSet("dc=x"),
28 | },
29 | {
30 | Addresses: []string{"3.3.3.3"},
31 | Labels: pb.ParseLabelSet("dc=y"),
32 | },
33 | }
34 |
35 | best, err := FindBest(&BestInput{
36 | Count: 10,
37 | Local: lloc,
38 | Remote: rloc,
39 | })
40 | require.NoError(t, err)
41 |
42 | require.Equal(t, 2, len(best))
43 |
44 | assert.Equal(t, "2.2.2.2", best[0].Addresses[0])
45 | })
46 |
47 | t.Run("find locations that with the highest cardinality", func(t *testing.T) {
48 | lloc := []*pb.NetworkLocation{
49 | {
50 | Addresses: []string{"1.1.1.1"},
51 | Labels: pb.ParseLabelSet("dc=x,az=y"),
52 | },
53 | }
54 |
55 | rloc := []*pb.NetworkLocation{
56 | {
57 | Addresses: []string{"2.2.2.2"},
58 | Labels: pb.ParseLabelSet("dc=x,az=q"),
59 | },
60 | {
61 | Addresses: []string{"3.3.3.3"},
62 | Labels: pb.ParseLabelSet("dc=y"),
63 | },
64 | }
65 |
66 | best, err := FindBest(&BestInput{
67 | Count: 10,
68 | Local: lloc,
69 | Remote: rloc,
70 | })
71 | require.NoError(t, err)
72 |
73 | require.Equal(t, 2, len(best))
74 |
75 | assert.Equal(t, "2.2.2.2", best[0].Addresses[0])
76 | })
77 |
78 | t.Run("can use latencys of the hosts to sort them", func(t *testing.T) {
79 | lloc := []*pb.NetworkLocation{
80 | {
81 | Addresses: []string{"1.1.1.1"},
82 | Labels: pb.ParseLabelSet("dc=x,az=y"),
83 | },
84 | }
85 |
86 | rloc := []*pb.NetworkLocation{
87 | {
88 | Addresses: []string{"2.2.2.2"},
89 | Labels: pb.ParseLabelSet("dc=x,az=q"),
90 | },
91 | {
92 | Addresses: []string{"3.3.3.3"},
93 | Labels: pb.ParseLabelSet("dc=y"),
94 | },
95 | }
96 |
97 | best, err := FindBest(&BestInput{
98 | Count: 10,
99 | Local: lloc,
100 | Remote: rloc,
101 | Latency: func(addr string) error {
102 | if addr == "2.2.2.2" {
103 | time.Sleep(time.Second)
104 | }
105 |
106 | return nil
107 | },
108 | })
109 | require.NoError(t, err)
110 |
111 | require.Equal(t, 2, len(best))
112 |
113 | assert.Equal(t, "3.3.3.3", best[0].Addresses[0])
114 | })
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/control/client_edge.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 | "fmt"
9 | "time"
10 |
11 | lru "github.com/hashicorp/golang-lru"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | )
14 |
15 | type clientEdgeData struct {
16 | edgeServiceCache *lru.ARCCache
17 | edgeLLCache *lru.ARCCache
18 | }
19 |
20 | func (ed *clientEdgeData) init() {
21 | ed.edgeServiceCache, _ = lru.NewARC(1000)
22 | ed.edgeLLCache, _ = lru.NewARC(1000)
23 | }
24 |
25 | type edgeServiceCacheEntry struct {
26 | resp *pb.LookupEndpointsResponse
27 | rc *RouteCalculation
28 | expiresAt time.Time
29 | }
30 |
31 | type edgeLLCacheEntry struct {
32 | resp *pb.ResolveLabelLinkResponse
33 | expiresAt time.Time
34 | }
35 |
36 | func (c *Client) lookupServiceEdge(ctx context.Context, account *pb.Account, labels *pb.LabelSet) (*RouteCalculation, error) {
37 | cacheKey := fmt.Sprintf("%s-%s", account.StringKey(), labels.SpecString())
38 |
39 | now := time.Now()
40 |
41 | val, ok := c.edgeData.edgeServiceCache.Get(cacheKey)
42 | if ok {
43 | ec := val.(*edgeServiceCacheEntry)
44 | if ec.expiresAt.After(now) {
45 | return ec.rc, nil
46 | }
47 | }
48 |
49 | resp, err := c.edge.LookupEndpoints(ctx, &pb.LookupEndpointsRequest{
50 | Account: account,
51 | Labels: labels,
52 | })
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | rc := &RouteCalculation{
58 | instanceId: c.instanceId,
59 | All: resp.Routes,
60 | }
61 | rc.FindBest()
62 |
63 | if resp.CacheTime > 0 {
64 | ec := &edgeServiceCacheEntry{
65 | resp: resp,
66 | rc: rc,
67 | expiresAt: now.Add(time.Duration(resp.CacheTime) * time.Second),
68 | }
69 |
70 | c.edgeData.edgeServiceCache.Add(cacheKey, ec)
71 | }
72 |
73 | return rc, nil
74 | }
75 |
76 | func (c *Client) resolveLLEdge(label *pb.LabelSet) (*pb.Account, *pb.LabelSet, *pb.Account_Limits, error) {
77 | cacheKey := label.SpecString()
78 |
79 | var resp *pb.ResolveLabelLinkResponse
80 |
81 | now := time.Now()
82 |
83 | val, ok := c.edgeData.edgeServiceCache.Get(cacheKey)
84 | if ok {
85 | ec := val.(*edgeLLCacheEntry)
86 | if ec.expiresAt.After(now) {
87 | resp = ec.resp
88 | }
89 | }
90 |
91 | var err error
92 |
93 | if resp == nil {
94 | resp, err = c.edge.ResolveLabelLink(context.Background(), &pb.ResolveLabelLinkRequest{
95 | Labels: label,
96 | })
97 |
98 | if err != nil {
99 | return nil, nil, nil, err
100 | }
101 |
102 | if resp.CacheTime > 0 {
103 | ec := &edgeLLCacheEntry{
104 | resp: resp,
105 | expiresAt: now.Add(time.Duration(resp.CacheTime) * time.Second),
106 | }
107 |
108 | c.edgeData.edgeLLCache.Add(cacheKey, ec)
109 | }
110 | }
111 |
112 | return resp.Account, resp.Labels, resp.Limits, nil
113 | }
114 |
--------------------------------------------------------------------------------
/pkg/agent/connect.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "io"
8 | "net"
9 | "time"
10 |
11 | "github.com/hashicorp/horizon/pkg/pb"
12 | "github.com/hashicorp/horizon/pkg/wire"
13 | "github.com/hashicorp/yamux"
14 | "github.com/pierrec/lz4/v3"
15 | "github.com/pkg/errors"
16 | )
17 |
18 | type Conn struct {
19 | io.Reader
20 | io.WriteCloser
21 |
22 | Stream *yamux.Stream
23 | Labels *pb.LabelSet
24 |
25 | cleanup []func() error
26 | }
27 |
28 | type agentConnAddr struct {
29 | labels *pb.LabelSet
30 | }
31 |
32 | func (c *Conn) Close() error {
33 | c.WriteCloser.Close()
34 | err := c.Stream.Close()
35 |
36 | for _, f := range c.cleanup {
37 | f()
38 | }
39 |
40 | return err
41 | }
42 |
43 | func (a *agentConnAddr) Network() string {
44 | return "hzn"
45 | }
46 |
47 | func (a *agentConnAddr) String() string {
48 | if a.labels == nil {
49 | return "type=local"
50 | }
51 |
52 | return a.labels.SpecString()
53 | }
54 |
55 | // LocalAddr returns the local network address.
56 | func (c *Conn) LocalAddr() net.Addr {
57 | return &agentConnAddr{}
58 | }
59 |
60 | // RemoteAddr returns the remote network address.
61 | func (c *Conn) RemoteAddr() net.Addr {
62 | return &agentConnAddr{labels: c.Labels}
63 | }
64 |
65 | func (c *Conn) SetDeadline(t time.Time) error {
66 | return c.Stream.SetDeadline(t)
67 | }
68 |
69 | func (c *Conn) SetReadDeadline(t time.Time) error {
70 | return c.Stream.SetReadDeadline(t)
71 | }
72 |
73 | func (c *Conn) SetWriteDeadline(t time.Time) error {
74 | return c.Stream.SetWriteDeadline(t)
75 | }
76 |
77 | func (a *Agent) Connect(labels *pb.LabelSet) (net.Conn, error) {
78 | a.mu.Lock()
79 | stream, err := a.sessions[0].OpenStream()
80 | a.mu.Unlock()
81 |
82 | if err != nil {
83 | return nil, errors.Wrapf(err, "error opening new yamux stream")
84 | }
85 |
86 | sr := lz4.NewReader(stream)
87 | sw := lz4.NewWriter(stream)
88 |
89 | fw, err := wire.NewFramingWriter(sw)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | fr, err := wire.NewFramingReader(sr)
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | var conreq pb.ConnectRequest
100 | conreq.Target = labels
101 |
102 | _, err = fw.WriteMarshal(1, &conreq)
103 | if err != nil {
104 | return nil, errors.Wrapf(err, "error writing connect request")
105 | }
106 |
107 | var ack pb.ConnectAck
108 |
109 | tag, _, err := fr.ReadMarshal(&ack)
110 | if err != nil {
111 | return nil, err
112 | }
113 |
114 | if tag != 1 {
115 | return nil, wire.ErrProtocolError
116 | }
117 |
118 | ctx := wire.NewContext(nil, fr, fw)
119 |
120 | r := ctx.Reader()
121 | w := ctx.Writer()
122 |
123 | return &Conn{
124 | Reader: r,
125 | WriteCloser: w,
126 | Stream: stream,
127 | cleanup: []func() error{sw.Close},
128 | }, nil
129 | }
130 |
--------------------------------------------------------------------------------
/pkg/pb/token.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: token.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *Headers) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *Headers) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *Signature) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *Signature) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
48 | // MarshalJSON implements json.Marshaler
49 | func (msg *TokenCapability) MarshalJSON() ([]byte, error) {
50 | var buf bytes.Buffer
51 | err := (&jsonpb.Marshaler{
52 | EnumsAsInts: false,
53 | EmitDefaults: false,
54 | OrigName: false,
55 | }).Marshal(&buf, msg)
56 | return buf.Bytes(), err
57 | }
58 |
59 | // UnmarshalJSON implements json.Unmarshaler
60 | func (msg *TokenCapability) UnmarshalJSON(b []byte) error {
61 | return (&jsonpb.Unmarshaler{
62 | AllowUnknownFields: false,
63 | }).Unmarshal(bytes.NewReader(b), msg)
64 | }
65 |
66 | // MarshalJSON implements json.Marshaler
67 | func (msg *Token) MarshalJSON() ([]byte, error) {
68 | var buf bytes.Buffer
69 | err := (&jsonpb.Marshaler{
70 | EnumsAsInts: false,
71 | EmitDefaults: false,
72 | OrigName: false,
73 | }).Marshal(&buf, msg)
74 | return buf.Bytes(), err
75 | }
76 |
77 | // UnmarshalJSON implements json.Unmarshaler
78 | func (msg *Token) UnmarshalJSON(b []byte) error {
79 | return (&jsonpb.Unmarshaler{
80 | AllowUnknownFields: false,
81 | }).Unmarshal(bytes.NewReader(b), msg)
82 | }
83 |
84 | // MarshalJSON implements json.Marshaler
85 | func (msg *Token_Body) MarshalJSON() ([]byte, error) {
86 | var buf bytes.Buffer
87 | err := (&jsonpb.Marshaler{
88 | EnumsAsInts: false,
89 | EmitDefaults: false,
90 | OrigName: false,
91 | }).Marshal(&buf, msg)
92 | return buf.Bytes(), err
93 | }
94 |
95 | // UnmarshalJSON implements json.Unmarshaler
96 | func (msg *Token_Body) UnmarshalJSON(b []byte) error {
97 | return (&jsonpb.Unmarshaler{
98 | AllowUnknownFields: false,
99 | }).Unmarshal(bytes.NewReader(b), msg)
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/grpc/lz4/lz4.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 LINE Corporation
2 | //
3 | // LINE Corporation licenses this file to you under the Apache License,
4 | // version 2.0 (the "License"); you may not use this file except in compliance
5 | // with the License. You may obtain a copy of the License at:
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package lz4
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "io/ioutil"
21 | "sync"
22 | "sync/atomic"
23 |
24 | "github.com/pierrec/lz4/v3"
25 | "google.golang.org/grpc/encoding"
26 | )
27 |
28 | // Name is the name registered for the gzip compressor.
29 | const Name = "lz4"
30 |
31 | const (
32 | // DefaultCompressionLevel default compression level (0=fastest)
33 | DefaultCompressionLevel = 0
34 |
35 | // BestCompressionLevel best but slowest compression level
36 | BestCompressionLevel = 16
37 | )
38 |
39 | func init() {
40 | c := &compressor{}
41 | c.poolCompressor.New = func() interface{} {
42 | return &writer{Writer: lz4.NewWriter(ioutil.Discard), pool: &c.poolCompressor}
43 | }
44 | encoding.RegisterCompressor(c)
45 | }
46 |
47 | var compressionLevel int32
48 |
49 | type writer struct {
50 | *lz4.Writer
51 | pool *sync.Pool
52 | }
53 |
54 | // SetLevel thread-safe sets compression level.
55 | func SetLevel(level int) error {
56 | if level < DefaultCompressionLevel || level > BestCompressionLevel {
57 | return fmt.Errorf("grpc: invalid gzip compression level: %d", level)
58 | }
59 | atomic.StoreInt32(&compressionLevel, int32(level))
60 | return nil
61 | }
62 |
63 | func (c *compressor) Compress(w io.Writer) (io.WriteCloser, error) {
64 | z := c.poolCompressor.Get().(*writer)
65 | z.Writer.CompressionLevel = int(atomic.LoadInt32(&compressionLevel))
66 | z.Writer.Reset(w)
67 | return z, nil
68 | }
69 |
70 | func (z *writer) Close() (err error) {
71 | err = z.Writer.Close()
72 | z.pool.Put(z)
73 | return
74 | }
75 |
76 | type reader struct {
77 | *lz4.Reader
78 | pool *sync.Pool
79 | }
80 |
81 | func (c *compressor) Decompress(r io.Reader) (io.Reader, error) {
82 | z, inPool := c.poolDecompressor.Get().(*reader)
83 | if inPool {
84 | z.Reset(r)
85 | return z, nil
86 | }
87 | return &reader{Reader: lz4.NewReader(r), pool: &c.poolDecompressor}, nil
88 | }
89 |
90 | func (z *reader) Read(p []byte) (n int, err error) {
91 | if n, err = z.Reader.Read(p); err == io.EOF {
92 | z.pool.Put(z)
93 | }
94 |
95 | return
96 | }
97 |
98 | func (c *compressor) Name() string {
99 | return Name
100 | }
101 |
102 | type compressor struct {
103 | poolCompressor sync.Pool
104 | poolDecompressor sync.Pool
105 | }
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Horizon Network Service
2 |
3 | This is the Horizon Network Service project. It provides the ability for individual agents to advertise
4 | and connect to other agents on a hosted set of network hubs.
5 |
6 | It is explicitly designed for operational robustness via simplicity. For instance, the hub components can
7 | run when completely cut off from the control plane (called the central tier) and will gracefully rejoin when
8 | the control plane is restored.
9 |
10 | It relies on S3 for long term, persistent service routing information.
11 |
12 |
13 | ### Architectural Notes
14 |
15 | #### Activity
16 |
17 | There are 2 separate systems that involving passing information about events that have occured and both are
18 | named activity.
19 |
20 | One passes data between central tier services using postgresql, by writing records to a table
21 | and issuing a postgresql `NOTIFY` command. This is used to flood routing updates on the control plane.
22 | Future work on the control plane may remove this system in favor of using a format messaging service, but
23 | for now it serves it's purpose fine and reduces the dependencies that the control plane has.
24 |
25 | The second activity system is one used between central tier services and hubs. Hubs make a long running
26 | gRPC bidirectional stream connection to the central tier which is picked up by one of individual running
27 | instances. This stream is used to pass activity like routing updates but also statitics the hubs hold to
28 | the control plane.
29 |
30 | ### Dev
31 |
32 | To make development of the system easier, there is an explicit dev mode. First, run:
33 |
34 | ```
35 | make dev-setup
36 | ```
37 |
38 | This will create the PostgreSQL database, migrate it, and bring up the other services with
39 | docker-compose.
40 |
41 | Next, you can run a control service node AND a hub in dev mode in the same process:
42 |
43 | ```
44 | go run ./cmd/hzn/main.go dev
45 | ```
46 |
47 | Next, you can connect an agent to this dev hub by specifying the control address as `dev://localhost:24403`.
48 | If you're connecting to the hub from docker, you should use `dev://docker.for.mac.localhost:24403`.
49 |
50 | #### Ports
51 |
52 | This setup exposes the services on the following ports:
53 |
54 | - _24401_: Control server GRPC
55 | - _24402_: Control server HTTP
56 | - _24403_: Hub HZN protocol handler
57 | - _24404_: Hub HTTP router
58 |
59 | #### Tokens and IDs
60 |
61 | The dev command writes 3 files that can be used to connect to them:
62 |
63 | - _dev_agent-token.txt_: A token to connect to the hub with
64 | - _dev-agent-id.txt_: The account ID tied to the auto created token
65 | - _dev-mgmt-token.txt_: A management client token that can be used to configure accounts, tokens, etc.
66 |
67 | #### HTTP Routing
68 |
69 | The easiest way to test the HTTP routing is to just fake a Host header. Here is an example of configuring
70 | a label-link and then sending the Hub's HTTP router a request:
71 |
72 | ```
73 | # Run an agent that advertises an http service
74 | $ go run ./cmd/hznagent agent --control dev://localhost:24403 --token "$(< dev-agent-token.txt)" --http 8081 --labels service=test,env=test --verbose
75 |
76 | # Setup a label link from test.alpha.waypoint.run to the above labels
77 | $ go run ./cmd/hznctl/main.go create-label-link --control-addr localhost:24401 --token "$(< dev-mgmt-token.txt)" --label :hostname=test.alpha.waypoint.run --account "$(< dev-agent-id.txt)" --target "service=test,env=test" --insecure
78 |
79 | # Make a request to the HTTP routers with a Host header that matches the label link
80 | $ curl -H "Host: test.alpha.waypoint.run" localhost:24404
81 | ```
82 |
83 |
--------------------------------------------------------------------------------
/pkg/pb/flow.pb.json.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-json. DO NOT EDIT.
2 | // source: flow.proto
3 |
4 | package pb
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/golang/protobuf/jsonpb"
10 | )
11 |
12 | // MarshalJSON implements json.Marshaler
13 | func (msg *FlowStream) MarshalJSON() ([]byte, error) {
14 | var buf bytes.Buffer
15 | err := (&jsonpb.Marshaler{
16 | EnumsAsInts: false,
17 | EmitDefaults: false,
18 | OrigName: false,
19 | }).Marshal(&buf, msg)
20 | return buf.Bytes(), err
21 | }
22 |
23 | // UnmarshalJSON implements json.Unmarshaler
24 | func (msg *FlowStream) UnmarshalJSON(b []byte) error {
25 | return (&jsonpb.Unmarshaler{
26 | AllowUnknownFields: false,
27 | }).Unmarshal(bytes.NewReader(b), msg)
28 | }
29 |
30 | // MarshalJSON implements json.Marshaler
31 | func (msg *FlowRecord) MarshalJSON() ([]byte, error) {
32 | var buf bytes.Buffer
33 | err := (&jsonpb.Marshaler{
34 | EnumsAsInts: false,
35 | EmitDefaults: false,
36 | OrigName: false,
37 | }).Marshal(&buf, msg)
38 | return buf.Bytes(), err
39 | }
40 |
41 | // UnmarshalJSON implements json.Unmarshaler
42 | func (msg *FlowRecord) UnmarshalJSON(b []byte) error {
43 | return (&jsonpb.Unmarshaler{
44 | AllowUnknownFields: false,
45 | }).Unmarshal(bytes.NewReader(b), msg)
46 | }
47 |
48 | // MarshalJSON implements json.Marshaler
49 | func (msg *FlowRecord_AgentConnection) MarshalJSON() ([]byte, error) {
50 | var buf bytes.Buffer
51 | err := (&jsonpb.Marshaler{
52 | EnumsAsInts: false,
53 | EmitDefaults: false,
54 | OrigName: false,
55 | }).Marshal(&buf, msg)
56 | return buf.Bytes(), err
57 | }
58 |
59 | // UnmarshalJSON implements json.Unmarshaler
60 | func (msg *FlowRecord_AgentConnection) UnmarshalJSON(b []byte) error {
61 | return (&jsonpb.Unmarshaler{
62 | AllowUnknownFields: false,
63 | }).Unmarshal(bytes.NewReader(b), msg)
64 | }
65 |
66 | // MarshalJSON implements json.Marshaler
67 | func (msg *FlowRecord_HubStats) MarshalJSON() ([]byte, error) {
68 | var buf bytes.Buffer
69 | err := (&jsonpb.Marshaler{
70 | EnumsAsInts: false,
71 | EmitDefaults: false,
72 | OrigName: false,
73 | }).Marshal(&buf, msg)
74 | return buf.Bytes(), err
75 | }
76 |
77 | // UnmarshalJSON implements json.Unmarshaler
78 | func (msg *FlowRecord_HubStats) UnmarshalJSON(b []byte) error {
79 | return (&jsonpb.Unmarshaler{
80 | AllowUnknownFields: false,
81 | }).Unmarshal(bytes.NewReader(b), msg)
82 | }
83 |
84 | // MarshalJSON implements json.Marshaler
85 | func (msg *FlowTopSnapshot) MarshalJSON() ([]byte, error) {
86 | var buf bytes.Buffer
87 | err := (&jsonpb.Marshaler{
88 | EnumsAsInts: false,
89 | EmitDefaults: false,
90 | OrigName: false,
91 | }).Marshal(&buf, msg)
92 | return buf.Bytes(), err
93 | }
94 |
95 | // UnmarshalJSON implements json.Unmarshaler
96 | func (msg *FlowTopSnapshot) UnmarshalJSON(b []byte) error {
97 | return (&jsonpb.Unmarshaler{
98 | AllowUnknownFields: false,
99 | }).Unmarshal(bytes.NewReader(b), msg)
100 | }
101 |
102 | // MarshalJSON implements json.Marshaler
103 | func (msg *FlowTopRequest) MarshalJSON() ([]byte, error) {
104 | var buf bytes.Buffer
105 | err := (&jsonpb.Marshaler{
106 | EnumsAsInts: false,
107 | EmitDefaults: false,
108 | OrigName: false,
109 | }).Marshal(&buf, msg)
110 | return buf.Bytes(), err
111 | }
112 |
113 | // UnmarshalJSON implements json.Unmarshaler
114 | func (msg *FlowTopRequest) UnmarshalJSON(b []byte) error {
115 | return (&jsonpb.Unmarshaler{
116 | AllowUnknownFields: false,
117 | }).Unmarshal(bytes.NewReader(b), msg)
118 | }
119 |
--------------------------------------------------------------------------------
/pkg/control/server_http.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | "encoding/json"
8 | fmt "fmt"
9 | "net"
10 | "net/http"
11 | "strings"
12 |
13 | "github.com/hashicorp/horizon/pkg/dbx"
14 | "github.com/hashicorp/horizon/pkg/discovery"
15 | "github.com/hashicorp/horizon/pkg/pb"
16 | )
17 |
18 | func (s *Server) GetAllNetworkLocations() ([]*pb.NetworkLocation, error) {
19 | var hubs []*Hub
20 |
21 | err := dbx.Check(s.db.Find(&hubs))
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | var locs []*pb.NetworkLocation
27 |
28 | for _, h := range hubs {
29 | var hl []*pb.NetworkLocation
30 |
31 | err = json.Unmarshal(h.ConnectionInfo, &hl)
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | for _, loc := range hl {
37 | loc.Name = h.StableIdULID().String() + "." + s.hubDomain
38 | }
39 |
40 | locs = append(locs, hl...)
41 | }
42 |
43 | return locs, nil
44 | }
45 |
46 | func (s *Server) setupRoutes() {
47 | s.mux.HandleFunc("/healthz", s.httpHealthz)
48 | s.mux.HandleFunc("/ip-info", s.httpIPInfo)
49 | s.mux.HandleFunc("/ulid", s.genUlid)
50 |
51 | var wk discovery.WellKnown
52 | wk.GetNetlocs = s
53 |
54 | s.mux.Handle(discovery.HTTPPath, &wk)
55 | }
56 |
57 | func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
58 | s.mux.ServeHTTP(w, req)
59 | }
60 |
61 | func (s *Server) httpHealthz(w http.ResponseWriter, req *http.Request) {
62 | w.WriteHeader(200)
63 | }
64 |
65 | func (s *Server) genUlid(w http.ResponseWriter, req *http.Request) {
66 | u := pb.NewULID()
67 |
68 | if req.Header.Get("Accept") == "application/json" {
69 | json.NewEncoder(w).Encode(map[string]string{
70 | "ulid": u.String(),
71 | })
72 | } else {
73 | fmt.Fprintln(w, u.String())
74 | }
75 | }
76 |
77 | func ipFromForwardedForHeader(v string) string {
78 | sep := strings.Index(v, ",")
79 | if sep == -1 {
80 | return v
81 | }
82 | return v[:sep]
83 | }
84 |
85 | var trustHeaders = []string{"X-Real-IP", "X-Forwarded-For"}
86 |
87 | func ipFromRequest(r *http.Request) (net.IP, error) {
88 | remoteIP := ""
89 | for _, header := range trustHeaders {
90 | remoteIP = r.Header.Get(header)
91 | if http.CanonicalHeaderKey(header) == "X-Forwarded-For" {
92 | remoteIP = ipFromForwardedForHeader(remoteIP)
93 | }
94 | if remoteIP != "" {
95 | break
96 | }
97 | }
98 |
99 | if remoteIP == "" {
100 | host, _, err := net.SplitHostPort(r.RemoteAddr)
101 | if err != nil {
102 | return nil, err
103 | }
104 | remoteIP = host
105 | }
106 |
107 | ip := net.ParseIP(remoteIP)
108 | if ip == nil {
109 | return nil, fmt.Errorf("could not parse IP: %s", remoteIP)
110 | }
111 | return ip, nil
112 | }
113 |
114 | // Needs to mimic the ifconfig.co keys because that's the document schema
115 | // that's expected.
116 | type ipInfo struct {
117 | IP string `json:"ip"`
118 | ASN string `json:"asn,omitempty"`
119 | ASNOrg string `json:"asn_org,omitempty"`
120 | }
121 |
122 | func (s *Server) httpIPInfo(w http.ResponseWriter, req *http.Request) {
123 | ip, err := ipFromRequest(req)
124 | if err != nil {
125 | w.WriteHeader(500)
126 | return
127 | }
128 |
129 | var info ipInfo
130 | info.IP = ip.String()
131 |
132 | if s.asnDB != nil {
133 | if asnInfo, err := s.asnDB.ASN(ip); err == nil {
134 | info.ASN = fmt.Sprintf("AS%d", asnInfo.AutonomousSystemNumber)
135 | info.ASNOrg = asnInfo.AutonomousSystemOrganization
136 | }
137 | }
138 |
139 | json.NewEncoder(w).Encode(&info)
140 | }
141 |
--------------------------------------------------------------------------------
/pkg/token/validate.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "crypto/ed25519"
8 | "crypto/subtle"
9 | "time"
10 |
11 | "github.com/hashicorp/horizon/pkg/pb"
12 | "github.com/pkg/errors"
13 | "golang.org/x/crypto/blake2b"
14 | )
15 |
16 | var (
17 | ErrBadToken = errors.New("bad token")
18 | ErrNoLongerValid = errors.New("token no longer valid")
19 | )
20 |
21 | // Exposed as a function to be changed by the tests to mimic clock issues
22 | var timeNow = time.Now
23 |
24 | type ValidToken struct {
25 | Body *pb.Token_Body
26 | Token *pb.Token
27 | Raw []byte
28 | KeyId string
29 | }
30 |
31 | func checkTokenValidity(b *pb.Token_Body) error {
32 | now := timeNow()
33 |
34 | if now.Before(b.Id.Time()) {
35 | return ErrNoLongerValid
36 | }
37 |
38 | if b.ValidUntil == nil {
39 | return nil
40 | }
41 |
42 | if now.After(b.ValidUntil.Time()) {
43 | return ErrNoLongerValid
44 | }
45 |
46 | return nil
47 | }
48 |
49 | func CheckTokenHMAC(stoken string, key []byte) (*ValidToken, error) {
50 | token, err := RemoveArmor(stoken)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | if token[0] != Magic {
56 | return nil, ErrBadToken
57 | }
58 |
59 | var t pb.Token
60 |
61 | err = t.Unmarshal(token[1:])
62 | if err != nil {
63 | return nil, err
64 | }
65 |
66 | h, err := blake2b.New256(key)
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | h.Write(t.Body)
72 |
73 | computed := h.Sum(nil)
74 |
75 | var (
76 | keyId string
77 | ok bool
78 | )
79 |
80 | for _, sig := range t.Signatures {
81 | if sig.SigType != pb.BLAKE2HMAC {
82 | continue
83 | }
84 |
85 | if subtle.ConstantTimeCompare(computed, sig.Signature) == 1 {
86 | ok = true
87 | keyId = sig.KeyId
88 | break
89 | }
90 | }
91 |
92 | if !ok {
93 | return nil, errors.Wrapf(ErrBadToken, "no signatures matched (%d)", len(t.Signatures))
94 | }
95 |
96 | var body pb.Token_Body
97 |
98 | err = body.Unmarshal(t.Body)
99 | if err != nil {
100 | return nil, errors.Wrapf(err, "corruption in protected headers")
101 | }
102 |
103 | err = checkTokenValidity(&body)
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | vt := &ValidToken{
109 | Body: &body,
110 | Token: &t,
111 | Raw: token,
112 | KeyId: keyId,
113 | }
114 |
115 | return vt, nil
116 | }
117 |
118 | func CheckTokenED25519(stoken string, key ed25519.PublicKey) (*ValidToken, error) {
119 | token, err := RemoveArmor(stoken)
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | if token[0] != Magic {
125 | return nil, ErrBadToken
126 | }
127 |
128 | var t pb.Token
129 |
130 | err = t.Unmarshal(token[1:])
131 | if err != nil {
132 | return nil, err
133 | }
134 |
135 | var (
136 | keyId string
137 | ok bool
138 | )
139 |
140 | for _, sig := range t.Signatures {
141 | if sig.SigType != pb.ED25519 {
142 | continue
143 | }
144 |
145 | if ed25519.Verify(key, t.Body, sig.Signature) {
146 | keyId = sig.KeyId
147 | ok = true
148 | break
149 | }
150 | }
151 |
152 | if !ok {
153 | return nil, errors.Wrapf(ErrBadToken, "no signatures matched")
154 | }
155 |
156 | var body pb.Token_Body
157 |
158 | err = body.Unmarshal(t.Body)
159 | if err != nil {
160 | return nil, errors.Wrapf(err, "corruption in protected headers")
161 | }
162 |
163 | err = checkTokenValidity(&body)
164 | if err != nil {
165 | return nil, err
166 | }
167 |
168 | vt := &ValidToken{
169 | Body: &body,
170 | Token: &t,
171 | Raw: token,
172 | KeyId: keyId,
173 | }
174 |
175 | return vt, nil
176 | }
177 |
--------------------------------------------------------------------------------
/pkg/wire/rpc.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "context"
8 | "sync"
9 |
10 | "github.com/hashicorp/go-hclog"
11 | "github.com/hashicorp/horizon/pkg/pb"
12 | "github.com/hashicorp/yamux"
13 | "github.com/pkg/errors"
14 | )
15 |
16 | type RPCClient struct {
17 | stream *yamux.Stream
18 | }
19 |
20 | const rpcTag = 20
21 |
22 | func (r *RPCClient) Begin(host, path string, req Marshaller) (RPCContext, error) {
23 | var wreq pb.Request
24 | wreq.Type = pb.RPC
25 | wreq.Path = path
26 | wreq.Host = host
27 |
28 | fw, err := NewFramingWriter(r.stream)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | _, err = fw.WriteMarshal(1, &wreq)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | _, err = fw.WriteMarshal(rpcTag, req)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | fr, err := NewFramingReader(r.stream)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | return &rpcCtx{
49 | Context: &ctx{
50 | fr: fr,
51 | fw: fw,
52 | },
53 | }, nil
54 | }
55 |
56 | func (r *RPCClient) Call(host, path string, req Marshaller, resp Unmarshaller) error {
57 | var wreq pb.Request
58 | wreq.Type = pb.RPC
59 | wreq.Path = "/query/peers"
60 | wreq.Host = "agents.edge"
61 |
62 | fw, err := NewFramingWriter(r.stream)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | defer fw.Recycle()
68 |
69 | _, err = fw.WriteMarshal(1, &wreq)
70 | if err != nil {
71 | return err
72 | }
73 |
74 | _, err = fw.WriteMarshal(rpcTag, req)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | fr, err := NewFramingReader(r.stream)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | defer fr.Recycle()
85 |
86 | tag, _, err := fr.ReadMarshal(resp)
87 | if err != nil {
88 | return err
89 | }
90 |
91 | if tag != rpcTag {
92 | return errors.Wrapf(ErrProtocolError, "wrong tag recieved: %d", tag)
93 | }
94 |
95 | return nil
96 | }
97 |
98 | func NewRPCClient(stream *yamux.Stream) *RPCClient {
99 | return &RPCClient{
100 | stream: stream,
101 | }
102 | }
103 |
104 | type RPCContext interface {
105 | Context
106 | ReadRequest(v Unmarshaller) error
107 | WriteResponse(v Marshaller) error
108 | }
109 |
110 | type RPCHandler interface {
111 | HandleRPC(ctx context.Context, wctx RPCContext) error
112 | }
113 |
114 | type RPCServer struct {
115 | mu sync.RWMutex
116 | methods map[string]RPCHandler
117 | }
118 |
119 | func (r *RPCServer) AddMethod(name string, h RPCHandler) {
120 | r.mu.Lock()
121 | defer r.mu.Unlock()
122 |
123 | if r.methods == nil {
124 | r.methods = make(map[string]RPCHandler)
125 | }
126 |
127 | r.methods[name] = h
128 | }
129 |
130 | var ErrUnknownMethod = errors.New("unknown method requested")
131 |
132 | type ReadMarshaler interface {
133 | ReadMarshal(Unmarshaller) (byte, int, error)
134 | }
135 |
136 | type WriteMarshaler interface {
137 | WriteMarshal(byte, Marshaller) (int, error)
138 | }
139 |
140 | type rpcCtx struct {
141 | Context
142 | }
143 |
144 | func (r *rpcCtx) ReadRequest(v Unmarshaller) error {
145 | tag, err := r.Context.ReadMarshal(v)
146 | if err != nil {
147 | return err
148 | }
149 |
150 | if tag != rpcTag {
151 | return errors.Wrapf(ErrProtocolError, "incorrect tag: %d (expected %d)", tag, rpcTag)
152 | }
153 |
154 | return nil
155 | }
156 |
157 | func (r *rpcCtx) WriteResponse(v Marshaller) error {
158 | return r.Context.WriteMarshal(rpcTag, v)
159 | }
160 |
161 | func (r *RPCServer) HandleRequest(ctx context.Context, L hclog.Logger, wctx Context, wreq *pb.Request) error {
162 | r.mu.RLock()
163 | defer r.mu.RUnlock()
164 |
165 | handler, ok := r.methods[wreq.Path]
166 | if !ok {
167 | return errors.Wrapf(ErrUnknownMethod, "no handler for method: %s", wreq.Path)
168 | }
169 |
170 | return handler.HandleRPC(ctx, &rpcCtx{wctx})
171 | }
172 |
--------------------------------------------------------------------------------
/pkg/control/lockmgr.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 | io "io"
9 | "sync"
10 | "time"
11 |
12 | consul "github.com/hashicorp/consul/api"
13 | "github.com/pkg/errors"
14 | )
15 |
16 | type inmemLockMgr struct {
17 | mu sync.Mutex
18 | cond *sync.Cond
19 |
20 | locks map[string]bool
21 | values map[string]string
22 | }
23 |
24 | var ErrLocked = errors.New("locked")
25 |
26 | func (i *inmemLockMgr) GetLock(id, val string) (io.Closer, error) {
27 | i.mu.Lock()
28 | defer i.mu.Unlock()
29 |
30 | if i.cond == nil {
31 | i.cond = sync.NewCond(&i.mu)
32 | }
33 |
34 | if i.locks == nil {
35 | i.locks = make(map[string]bool)
36 | }
37 |
38 | if i.values == nil {
39 | i.values = make(map[string]string)
40 | }
41 |
42 | for {
43 | if i.locks[id] {
44 | i.cond.Wait()
45 | } else {
46 | break
47 | }
48 | }
49 |
50 | i.locks[id] = true
51 | i.values[id] = val
52 |
53 | return &inmemUnlock{i, id}, nil
54 | }
55 |
56 | func (i *inmemLockMgr) GetValue(id string) (string, error) {
57 | i.mu.Lock()
58 | defer i.mu.Unlock()
59 |
60 | return i.values[id], nil
61 | }
62 |
63 | type inmemUnlock struct {
64 | i *inmemLockMgr
65 | id string
66 | }
67 |
68 | func (i *inmemUnlock) Close() error {
69 | i.i.mu.Lock()
70 | defer i.i.mu.Unlock()
71 |
72 | i.i.locks[i.id] = false
73 | i.i.cond.Broadcast()
74 |
75 | return nil
76 | }
77 |
78 | func NewConsulLockManager(ctx context.Context) (*consulLockMgr, error) {
79 | cfg := consul.DefaultConfig()
80 | client, err := consul.NewClient(cfg)
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | session := client.Session()
86 |
87 | id, _, err := session.CreateNoChecks(&consul.SessionEntry{
88 | Name: "hzn",
89 | TTL: "10s",
90 | LockDelay: 5 * time.Second,
91 | }, nil)
92 |
93 | if err != nil {
94 | return nil, err
95 | }
96 |
97 | go session.RenewPeriodic("5s", id, nil, ctx.Done())
98 |
99 | lm := &consulLockMgr{
100 | ctx: ctx,
101 | client: client,
102 | session: id,
103 | localLock: make(map[string]bool),
104 | }
105 |
106 | lm.cond = sync.NewCond(&lm.mu)
107 |
108 | return lm, nil
109 | }
110 |
111 | type consulLockMgr struct {
112 | ctx context.Context
113 | client *consul.Client
114 | session string
115 |
116 | mu sync.Mutex
117 | cond *sync.Cond
118 | localLock map[string]bool
119 | }
120 |
121 | func (c *consulLockMgr) GetValue(id string) (string, error) {
122 | pair, _, err := c.client.KV().Get(id, nil)
123 | if err != nil {
124 | return "", err
125 | }
126 |
127 | if pair == nil {
128 | return "", nil
129 | }
130 |
131 | return string(pair.Value), nil
132 | }
133 |
134 | func (c *consulLockMgr) GetLock(id, val string) (io.Closer, error) {
135 | c.mu.Lock()
136 | defer c.mu.Unlock()
137 |
138 | for {
139 | if c.localLock[id] {
140 | c.cond.Wait()
141 | } else {
142 | break
143 | }
144 | }
145 |
146 | lock, err := c.client.LockOpts(&consul.LockOptions{
147 | Key: id,
148 | Value: []byte(val),
149 | Session: c.session,
150 | LockWaitTime: time.Second,
151 | })
152 | if err != nil {
153 | return nil, err
154 | }
155 |
156 | c.mu.Unlock()
157 | ch, err := lock.Lock(c.ctx.Done())
158 | c.mu.Lock()
159 |
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | if ch == nil {
165 | return nil, ErrLocked
166 | }
167 |
168 | c.localLock[id] = true
169 |
170 | return &consulUnlocker{c: c, id: id, lock: lock}, nil
171 | }
172 |
173 | type consulUnlocker struct {
174 | c *consulLockMgr
175 | id string
176 | lock *consul.Lock
177 | }
178 |
179 | func (c *consulUnlocker) Close() error {
180 | c.c.mu.Lock()
181 | defer c.c.mu.Unlock()
182 |
183 | delete(c.c.localLock, c.id)
184 | c.c.cond.Broadcast()
185 |
186 | return c.lock.Unlock()
187 | }
188 |
--------------------------------------------------------------------------------
/internal/sqljson/data_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package sqljson
5 |
6 | import (
7 | "encoding/json"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | // Test that Scan returns an error if an invalid value is given.
14 | func TestData_Scan_Invalid(t *testing.T) {
15 | cases := []struct {
16 | name string
17 | value interface{}
18 | }{{
19 | // String slices cannot be converted to []byte which is why this should
20 | // fail.
21 | name: "incompatible type",
22 | value: []string{"foo", "bar"},
23 | }, {
24 | // Only JSON encoded values can be scanned. Strings need to be quoted
25 | // to be valid JSON.
26 | name: "invalid JSON",
27 | value: []byte("foo"),
28 | }}
29 |
30 | for _, tc := range cases {
31 | t.Run(tc.name, func(t *testing.T) {
32 | r := require.New(t)
33 |
34 | m := Data{}
35 |
36 | r.Error(m.Scan(tc.value))
37 | })
38 | }
39 | }
40 |
41 | // Test that Scan populates a Data instance correctly.
42 | func TestData_Scan_Success(t *testing.T) {
43 | r := require.New(t)
44 |
45 | m := Data{}
46 |
47 | r.NoError(m.Scan([]byte(`{"foo": "bar"}`)))
48 |
49 | var v string
50 | ok, err := m.Get("foo", &v)
51 | r.True(ok)
52 | r.NoError(err)
53 |
54 | r.Equal("bar", v)
55 | }
56 |
57 | // Test that Value creates a JSON representation of the Data instance.
58 | func TestData_Value_Success(t *testing.T) {
59 | r := require.New(t)
60 |
61 | m := Data{}
62 | r.NoError(m.Set("foo", "bar"))
63 |
64 | val, err := m.Value()
65 | r.NoError(err)
66 | r.IsType([]byte{}, val)
67 |
68 | var res map[string]string
69 | r.NoError(json.Unmarshal(val.([]byte), &res))
70 | r.Equal("bar", res["foo"])
71 | }
72 |
73 | // Test that Get returns false but no error if a key does not exist. This should
74 | // be true if the Data itself is nil or if the Data is non-nil but does
75 | // not contain the requested key.
76 | func TestData_Get_NotFound(t *testing.T) {
77 | cases := []struct {
78 | name string
79 | m Data
80 | }{{
81 | name: "data is nil",
82 | m: nil,
83 | }, {
84 | name: "data is empty",
85 | m: Data{},
86 | }}
87 |
88 | for _, tc := range cases {
89 | t.Run(tc.name, func(t *testing.T) {
90 | r := require.New(t)
91 |
92 | var v interface{}
93 | ok, err := tc.m.Get("foo", &v)
94 |
95 | r.NoError(err)
96 | r.False(ok)
97 | })
98 | }
99 | }
100 |
101 | // Test that Get returns an error if invalid JSON is found in a Data value.
102 | func TestData_Get_Invalid(t *testing.T) {
103 | r := require.New(t)
104 |
105 | // create a Data instance with an invalid value. Note that JSON encoded
106 | // strings are quoted, thus `bar` is not valid.
107 | m := Data{
108 | "foo": []byte("bar"),
109 | }
110 |
111 | var v string
112 | ok, err := m.Get("foo", &v)
113 |
114 | r.Error(err)
115 | r.True(ok)
116 | r.Empty(v)
117 | }
118 |
119 | // Test that Get finds a value that was stored using Set previously.
120 | func TestData_SetGet_Success(t *testing.T) {
121 | cases := []struct {
122 | name string
123 | m Data
124 | }{{
125 | name: "data is nil",
126 | m: nil,
127 | }, {
128 | name: "data is empty",
129 | m: Data{},
130 | }}
131 |
132 | for _, tc := range cases {
133 | t.Run(tc.name, func(t *testing.T) {
134 | r := require.New(t)
135 |
136 | r.NoError(tc.m.Set("foo", "bar"))
137 |
138 | var v string
139 | ok, err := tc.m.Get("foo", &v)
140 |
141 | r.NoError(err)
142 | r.True(ok)
143 | r.Equal("bar", v)
144 | })
145 | }
146 | }
147 |
148 | // Test that Set returns an error if the value cannot be JSON encoded.
149 | func TestData_Set_Invalid(t *testing.T) {
150 | r := require.New(t)
151 |
152 | m := Data{}
153 |
154 | // Try to store a channel in Data. Channels cannot be JSON encoded which
155 | // is why this should fail.
156 | err := m.Set("foo", make(chan interface{}))
157 |
158 | r.IsType(&json.UnsupportedTypeError{}, err)
159 | }
160 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME := horizon
2 | GIT_COMMIT ?= $(shell git rev-parse --short HEAD)
3 |
4 | ## default to the old TeamCity-specific Docker tag prefix
5 | DOCKER_TAG_PREFIX ?= TC
6 |
7 | ## BUILD_COUNTER is set by TeamCity; if not provided, use Circle's equivalent
8 | BUILD_COUNTER ?= $(CIRCLE_BUILD_NUM)
9 |
10 | EFFECTIVE_LD_FLAGS ?= "-X main.GitCommit=$(GIT_COMMIT) $(LD_FLAGS)"
11 |
12 | ## fully-qualified path to this Makefile
13 | MKFILE_PATH := $(realpath $(lastword $(MAKEFILE_LIST)))
14 |
15 | ## fully-qualified path to the current directory
16 | CURRENT_DIR := $(patsubst %/,%,$(dir $(MKFILE_PATH)))
17 |
18 | ## all non-test source files
19 | GO_MOD_SOURCES := go.mod go.sum
20 | SOURCES := $(GO_MOD_SOURCES) $(shell go list -mod=readonly -f '{{range .GoFiles}}{{ $$.Dir }}/{{.}} {{end}}' ./... | sed -e 's@$(CURRENT_DIR)/@@g' )
21 | TEST_SOURCES := $(GO_MOD_SOURCES) $(shell go list -mod=readonly -f '{{range .XTestGoFiles}}{{ $$.Dir }}/{{.}} {{end}}' ./... | sed -e 's@$(CURRENT_DIR)/@@g' )
22 |
23 | .PHONY: help
24 | help:
25 | @echo "Valid targets:"
26 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MKFILE_PATH) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
27 |
28 | ## https://stackoverflow.com/a/36045843
29 | require-%:
30 | $(if ${${*}},,$(error You must pass the $* environment variable))
31 |
32 | ## run tests only when sources have change
33 | work/.tests-ran: $(SOURCES) $(TEST_SOURCES)
34 | go test -v ./... || exit 1
35 | @touch $@
36 |
37 | .PHONY: test
38 | test: work/.tests-ran ## Run tests
39 |
40 | .PHONY: bin
41 | BIN := bin/hzn
42 | bin: $(BIN) ## Build application binary
43 | $(BIN): $(SOURCES) | test
44 | go build -o $@ -ldflags $(EFFECTIVE_LD_FLAGS) ./cmd/hzn
45 |
46 | linux: ## Build a linux/amd64 version of the binary (mainly used for local development)
47 | GOOS=linux GOARCH=amd64 go build -o "$(BIN)" -ldflags $(EFFECTIVE_LD_FLAGS) ./cmd/hzn
48 |
49 | .PHONY: pkg
50 | pkg: pkg/$(NAME).tar.gz ## Build application 'serviceball'
51 | pkg/$(NAME).tar.gz: bin
52 | @mkdir -p $(dir $@)
53 | tar -czf $@ --xform='s,bin/,,' --xform='s,_build/,,' bin/*
54 |
55 | .PHONY: clean
56 | clean:
57 | rm -r $(CURDIR)/bin
58 | rm -r $(CURDIR)/pkg
59 | rm -r $(CURDIR)/work
60 |
61 | ## an error is raised if these are not set
62 | .PHONY: require-docker-vars
63 | require-docker-vars: require-DOCKER_USER require-DOCKER_PASS require-DOCKER_URL \
64 | require-BUILD_NUMBER require-BUILD_COUNTER
65 |
66 | DOCKER_IMAGE := $(DOCKER_URL)/$(DOCKER_ORG)/$(NAME)
67 | .PHONY: pkg-docker
68 | pkg-docker: require-docker-vars ## Creates a docker container and uploads it to the repo
69 | @echo $(DOCKER_PASS) | docker login --username "$(DOCKER_USER)" --password-stdin $(DOCKER_URL)
70 |
71 | DOCKER_BUILDKIT=1 docker build -t $(DOCKER_IMAGE):$(BUILD_NUMBER) .
72 |
73 | docker tag $(DOCKER_IMAGE):$(BUILD_NUMBER) $(DOCKER_IMAGE):$(DOCKER_TAG_PREFIX)$(BUILD_COUNTER)
74 |
75 | docker push $(DOCKER_IMAGE):$(DOCKER_TAG_PREFIX)$(BUILD_COUNTER)
76 | docker push $(DOCKER_IMAGE):$(BUILD_NUMBER)
77 |
78 |
79 | dev-setup:
80 | docker-compose exec -- postgres psql --username postgres -c "CREATE DATABASE horizon_dev;" || true
81 | DATABASE_URL=postgres://postgres:postgres@localhost/horizon_dev?sslmode=disable MIGRATIONS_PATH=./pkg/control/migrations go run ./cmd/hzn/main.go migrate || true
82 | docker-compose up -d
83 |
84 | dev-start:
85 | DATABASE_URL=postgres://postgres:postgres@localhost/horizon_dev?sslmode=disable go run ./cmd/hzn/ dev
86 |
87 |
88 |
89 | run-agent:
90 | go run ./cmd/hznagent agent \
91 | --control dev://localhost:24403 \
92 | --token "$$(< dev-agent-token.txt)" \
93 | --http 8085 \
94 | --labels service=test,env=test \
95 | --verbose
96 |
97 |
98 | run-label-link:
99 | go run ./cmd/hznctl/main.go \
100 | create-label-link \
101 | --control-addr localhost:24401 \
102 | --token "$$(< dev-mgmt-token.txt)" \
103 | --label :hostname=app-1.waypoint.local:24404 \
104 | --account "$$(< dev-agent-id.txt)" \
105 | --target "service=test,env=test" \
106 | --insecure
107 |
108 | .PHONY: dev-setup
109 |
--------------------------------------------------------------------------------
/pkg/wire/context.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "io"
8 | "sync"
9 | "sync/atomic"
10 |
11 | "github.com/hashicorp/go-multierror"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | "github.com/pkg/errors"
14 | )
15 |
16 | type Context interface {
17 | Account() *pb.Account
18 | ReadMarshal(v Unmarshaller) (byte, error)
19 | WriteMarshal(tag byte, v Marshaller) error
20 |
21 | // Forwards any data between the 2 contexts
22 | BridgeTo(other Context) error
23 |
24 | // Returns a writer that will send traffic as framed messages
25 | Writer() io.WriteCloser
26 |
27 | // Returns a reader that recieves traffic as framed messages
28 | Reader() io.Reader
29 |
30 | // Returns the total number of messages and bytes, respectively, that the
31 | // context has transmitted.
32 | Accounting() (int64, int64)
33 |
34 | // Close the context and cleanup any resources. Does not close
35 | // any IOs though.
36 | Close() error
37 | }
38 |
39 | type ctx struct {
40 | accountId *pb.Account
41 | fr *FramingReader
42 | fw *FramingWriter
43 |
44 | // accounting
45 | messages *int64
46 | bytes *int64
47 |
48 | closers []func() error
49 | }
50 |
51 | func NewContext(accountId *pb.Account, fr *FramingReader, fw *FramingWriter) Context {
52 | return &ctx{
53 | accountId: accountId,
54 | fr: fr,
55 | fw: fw,
56 | messages: new(int64),
57 | bytes: new(int64),
58 | }
59 | }
60 |
61 | func (c *ctx) Close() error {
62 | var err error
63 |
64 | for _, f := range c.closers {
65 | serr := f()
66 | if serr != nil {
67 | err = multierror.Append(err, serr)
68 | }
69 | }
70 |
71 | return err
72 | }
73 |
74 | type closeCtx struct {
75 | Context
76 |
77 | closers []func() error
78 | }
79 |
80 | func WithCloser(c Context, closers ...func() error) Context {
81 | if priv, ok := c.(*ctx); ok {
82 | priv.closers = append(priv.closers, closers...)
83 | return priv
84 | }
85 |
86 | return &closeCtx{Context: c, closers: closers}
87 | }
88 |
89 | func (cc *closeCtx) Close() error {
90 | var err error
91 |
92 | for _, f := range cc.closers {
93 | serr := f()
94 | if serr != nil {
95 | err = multierror.Append(err, serr)
96 | }
97 | }
98 |
99 | parent := cc.Context.Close()
100 | if parent != nil {
101 | err = multierror.Append(err, parent)
102 | }
103 |
104 | return err
105 | }
106 |
107 | func (c *ctx) Account() *pb.Account {
108 | return c.accountId
109 | }
110 |
111 | func (c *ctx) Accounting() (int64, int64) {
112 | return atomic.LoadInt64(c.messages), atomic.LoadInt64(c.bytes)
113 | }
114 |
115 | func (c *ctx) ReadMarshal(v Unmarshaller) (byte, error) {
116 | tag, _, err := c.fr.ReadMarshal(v)
117 | if err != nil {
118 | return 0, err
119 | }
120 |
121 | return tag, nil
122 | }
123 |
124 | func (c *ctx) WriteMarshal(tag byte, v Marshaller) error {
125 | sz, err := c.fw.WriteMarshal(tag, v)
126 | if err != nil {
127 | return err
128 | }
129 |
130 | atomic.AddInt64(c.messages, 1)
131 | atomic.AddInt64(c.bytes, int64(sz))
132 | return nil
133 | }
134 |
135 | func (c *ctx) Writer() io.WriteCloser {
136 | return c.fw.WriteAdapter()
137 | }
138 |
139 | func (c *ctx) Reader() io.Reader {
140 | return c.fr.ReadAdapter()
141 | }
142 |
143 | var ErrInvalidContext = errors.New("invalid context type")
144 |
145 | func (c *ctx) copyTo(octx *ctx) error {
146 | buf := make([]byte, 32*1024)
147 |
148 | for {
149 | tag, sz, err := c.fr.Next()
150 | if err != nil {
151 | return err
152 | }
153 |
154 | err = octx.fw.WriteFrame(tag, sz)
155 | if err != nil {
156 | return err
157 | }
158 |
159 | _, err = io.CopyBuffer(octx.fw, c.fr, buf)
160 | if err != nil {
161 | return err
162 | }
163 | }
164 | }
165 |
166 | func (c *ctx) BridgeTo(other Context) error {
167 | octx, ok := other.(*ctx)
168 | if !ok {
169 | return ErrInvalidContext
170 | }
171 |
172 | var wg sync.WaitGroup
173 |
174 | wg.Add(1)
175 | go func() {
176 | defer wg.Done()
177 | c.copyTo(octx)
178 | }()
179 |
180 | octx.copyTo(c)
181 |
182 | wg.Wait()
183 |
184 | return nil
185 | }
186 |
--------------------------------------------------------------------------------
/pkg/control/activity.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package control
5 |
6 | import (
7 | context "context"
8 | "encoding/json"
9 | "sync"
10 | "time"
11 |
12 | "github.com/hashicorp/go-hclog"
13 | "github.com/hashicorp/horizon/pkg/dbx"
14 | "github.com/jinzhu/gorm"
15 | "github.com/lib/pq"
16 | )
17 |
18 | // A quick note. This activity system is different than the one used between the hubs and control.
19 | // This system is for managing an activity log that is in postgresql and shared between central
20 | // teir instances. We use it instead of a message queue system right now for simplicity.
21 |
22 | type ActivityLog struct {
23 | Id int64 `gorm:"primary_key"`
24 | Event []byte
25 | CreatedAt time.Time
26 | }
27 |
28 | type ActivityReader struct {
29 | db *gorm.DB
30 | listener *pq.Listener
31 |
32 | lastEntry int64
33 | cancel func()
34 |
35 | C chan []*ActivityLog
36 |
37 | wg sync.WaitGroup
38 | }
39 |
40 | var pgActivityChannel = "activaty_added"
41 |
42 | func NewActivityReader(ctx context.Context, dbtype, conn string) (*ActivityReader, error) {
43 | db, err := gorm.Open(dbtype, conn)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | L := hclog.FromContext(ctx)
49 |
50 | reportProblem := func(ev pq.ListenerEventType, err error) {
51 | if err != nil {
52 | L.Error("problem observed while listen on postgres channel", "error", err)
53 | }
54 | }
55 |
56 | minReconn := 10 * time.Second
57 | maxReconn := time.Minute
58 | listener := pq.NewListener(conn, minReconn, maxReconn, reportProblem)
59 |
60 | err = listener.Listen(pgActivityChannel)
61 | if err != nil {
62 | db.Close()
63 | return nil, err
64 | }
65 |
66 | var entry ActivityLog
67 |
68 | err = dbx.Check(db.Last(&entry))
69 | if err != nil {
70 | if err != gorm.ErrRecordNotFound {
71 | return nil, err
72 | }
73 | }
74 |
75 | ctx, cancel := context.WithCancel(ctx)
76 |
77 | ar := &ActivityReader{
78 | db: db,
79 | listener: listener,
80 | C: make(chan []*ActivityLog),
81 | lastEntry: entry.Id,
82 | cancel: cancel,
83 | }
84 |
85 | ar.wg.Add(1)
86 | go ar.watch(ctx, L)
87 |
88 | return ar, nil
89 | }
90 |
91 | func (ar *ActivityReader) watch(ctx context.Context, L hclog.Logger) {
92 | defer ar.wg.Done()
93 |
94 | ticker := time.NewTicker(time.Minute)
95 | defer ticker.Stop()
96 |
97 | for {
98 | select {
99 | case <-ctx.Done():
100 | return
101 | case <-ar.listener.Notify:
102 | // got event
103 | case <-ticker.C:
104 | // timed out, check
105 | }
106 |
107 | ar.checkLog(ctx, L)
108 | }
109 | }
110 |
111 | func (ar *ActivityReader) checkLog(ctx context.Context, L hclog.Logger) {
112 | for {
113 | var entries []*ActivityLog
114 |
115 | err := dbx.Check(ar.db.Where("id > ?", ar.lastEntry).Limit(100).Find(&entries))
116 | if err != nil {
117 | if err != gorm.ErrRecordNotFound {
118 | L.Error("error looking for new activity log entries", "error", err)
119 | }
120 |
121 | return
122 | }
123 |
124 | if len(entries) == 0 {
125 | return
126 | }
127 |
128 | select {
129 | case <-ctx.Done():
130 | return
131 | case ar.C <- entries:
132 | // ok
133 | }
134 |
135 | ar.lastEntry = entries[len(entries)-1].Id
136 | }
137 | }
138 |
139 | func (ar *ActivityReader) Close() error {
140 | ar.cancel()
141 | ar.wg.Wait()
142 | return ar.db.Close()
143 | }
144 |
145 | type ActivityInjector struct {
146 | db *gorm.DB
147 | }
148 |
149 | func NewActivityInjector(db *gorm.DB) (*ActivityInjector, error) {
150 | ai := &ActivityInjector{
151 | db: db,
152 | }
153 |
154 | return ai, nil
155 | }
156 |
157 | func (ai *ActivityInjector) Inject(ctx context.Context, v interface{}) error {
158 | var entry ActivityLog
159 |
160 | switch sv := v.(type) {
161 | case []byte:
162 | entry.Event = sv
163 | default:
164 | data, err := json.Marshal(v)
165 | if err != nil {
166 | return err
167 | }
168 | entry.Event = data
169 | }
170 |
171 | tx := ai.db.Begin()
172 | tx.Create(&entry)
173 | tx.Exec("NOTIFY " + pgActivityChannel)
174 |
175 | return dbx.Check(tx.Commit())
176 | }
177 |
--------------------------------------------------------------------------------
/pkg/hub/consul_health_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package hub
5 |
6 | import (
7 | "context"
8 | "sort"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 |
15 | consul "github.com/hashicorp/consul/api"
16 | "github.com/hashicorp/go-hclog"
17 | )
18 |
19 | func TestConsulHealth(t *testing.T) {
20 | t.Run("maintains a proper view of the hub catalog", func(t *testing.T) {
21 | cfg := consul.DefaultConfig()
22 | a, err := NewConsulHealth("a", cfg)
23 | require.NoError(t, err)
24 |
25 | b, err := NewConsulHealth("b", cfg)
26 | require.NoError(t, err)
27 |
28 | parent, cancel := context.WithCancel(context.Background())
29 | defer cancel()
30 |
31 | achk, err := a.registerService(parent)
32 | require.NoError(t, err)
33 |
34 | defer a.deregisterService(parent)
35 |
36 | err = a.passCheck(achk)
37 | require.NoError(t, err)
38 |
39 | bchk, err := b.registerService(parent)
40 | require.NoError(t, err)
41 |
42 | defer b.deregisterService(parent)
43 |
44 | err = b.passCheck(bchk)
45 | require.NoError(t, err)
46 |
47 | _, err = a.refreshHubs(parent, 0, time.Second)
48 | require.NoError(t, err)
49 |
50 | _, err = b.refreshHubs(parent, 0, time.Second)
51 | require.NoError(t, err)
52 |
53 | a.mu.Lock()
54 |
55 | var hubs []string
56 |
57 | for k := range a.hubs {
58 | hubs = append(hubs, k)
59 | }
60 |
61 | sort.Strings(hubs)
62 |
63 | a.mu.Unlock()
64 |
65 | assert.Equal(t, []string{"a", "b"}, hubs)
66 |
67 | assert.True(t, a.Available("b"))
68 |
69 | err = b.deregisterService(parent)
70 | require.NoError(t, err)
71 |
72 | time.Sleep(time.Second)
73 |
74 | _, err = a.refreshHubs(parent, 0, time.Second)
75 | require.NoError(t, err)
76 |
77 | a.mu.Lock()
78 |
79 | hubs = nil
80 |
81 | for k := range a.hubs {
82 | hubs = append(hubs, k)
83 | }
84 |
85 | a.mu.Unlock()
86 |
87 | assert.False(t, a.Available("b"))
88 | assert.Equal(t, []string{"a"}, hubs)
89 | })
90 |
91 | t.Run("notices service changes without polling", func(t *testing.T) {
92 | cfg := consul.DefaultConfig()
93 | a, err := NewConsulHealth("a", cfg)
94 | require.NoError(t, err)
95 |
96 | b, err := NewConsulHealth("b", cfg)
97 | require.NoError(t, err)
98 |
99 | parent, cancel := context.WithCancel(context.Background())
100 | defer cancel()
101 |
102 | achk, err := a.registerService(parent)
103 | require.NoError(t, err)
104 |
105 | defer a.deregisterService(parent)
106 |
107 | err = a.passCheck(achk)
108 | require.NoError(t, err)
109 |
110 | bchk, err := b.registerService(parent)
111 | require.NoError(t, err)
112 |
113 | defer b.deregisterService(parent)
114 |
115 | err = b.passCheck(bchk)
116 | require.NoError(t, err)
117 |
118 | go a.Watch(parent, 10*time.Second)
119 |
120 | time.Sleep(time.Second)
121 |
122 | assert.True(t, a.Available("b"))
123 |
124 | err = b.deregisterService(parent)
125 | require.NoError(t, err)
126 |
127 | time.Sleep(100 * time.Millisecond)
128 |
129 | assert.False(t, a.Available("b"))
130 | })
131 |
132 | t.Run("has a comfortable default mode", func(t *testing.T) {
133 | cfg := consul.DefaultConfig()
134 | a, err := NewConsulHealth("a", cfg)
135 | require.NoError(t, err)
136 |
137 | b, err := NewConsulHealth("b", cfg)
138 | require.NoError(t, err)
139 |
140 | parent, cancel := context.WithCancel(context.Background())
141 | defer cancel()
142 |
143 | bctx, bcancel := context.WithCancel(parent)
144 |
145 | err = a.Start(parent, hclog.L())
146 | require.NoError(t, err)
147 |
148 | err = b.Start(bctx, hclog.L())
149 | require.NoError(t, err)
150 |
151 | time.Sleep(time.Second)
152 |
153 | assert.True(t, a.Available("b"))
154 |
155 | t.Log("waiting past the check TTL to make sure they stay alive")
156 |
157 | dur, err := time.ParseDuration(CheckTTL)
158 | require.NoError(t, err)
159 |
160 | time.Sleep(dur + (1 * time.Second))
161 |
162 | assert.True(t, a.Available("b"))
163 |
164 | t.Log("canceling b, watching it fall")
165 |
166 | bcancel()
167 |
168 | time.Sleep(100 * time.Millisecond)
169 |
170 | assert.False(t, a.Available("b"))
171 | })
172 | }
173 |
--------------------------------------------------------------------------------
/pkg/web/test/web_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package web_test
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "io/ioutil"
10 | "net/http"
11 | "net/http/httptest"
12 | "strings"
13 | "testing"
14 | "time"
15 |
16 | "github.com/hashicorp/go-hclog"
17 | "github.com/hashicorp/horizon/pkg/agent"
18 | "github.com/hashicorp/horizon/pkg/discovery"
19 | "github.com/hashicorp/horizon/pkg/hub"
20 | "github.com/hashicorp/horizon/pkg/pb"
21 | "github.com/hashicorp/horizon/pkg/testutils/central"
22 | "github.com/hashicorp/horizon/pkg/web"
23 | "github.com/stretchr/testify/assert"
24 | "github.com/stretchr/testify/require"
25 | )
26 |
27 | type fakeHTTPService struct {
28 | host string
29 | }
30 |
31 | func (f *fakeHTTPService) HandleRequest(ctx context.Context, L hclog.Logger, sctx agent.ServiceContext) error {
32 | var req pb.Request
33 |
34 | _, err := sctx.ReadMarshal(&req)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | f.host = req.Host
40 |
41 | var resp pb.Response
42 |
43 | data, err := ioutil.ReadAll(sctx.Reader())
44 | if err != nil {
45 | return err
46 | }
47 |
48 | resp.Headers = []*pb.Header{
49 | {
50 | Name: "X-Region",
51 | Value: []string{"test"},
52 | },
53 | }
54 |
55 | resp.Code = 247
56 |
57 | err = sctx.WriteMarshal(1, &resp)
58 | if err != nil {
59 | return err
60 | }
61 |
62 | w := sctx.Writer()
63 | fmt.Fprintf(w, "this is from the fake service: %s", string(data))
64 | return w.Close()
65 | }
66 |
67 | func TestWeb(t *testing.T) {
68 | central.Dev(t, func(setup *central.DevSetup) {
69 | L := hclog.L()
70 | hub, err := hub.NewHub(L, setup.ControlClient, setup.HubServToken)
71 | require.NoError(t, err)
72 |
73 | ctx, cancel := context.WithCancel(context.Background())
74 | defer cancel()
75 |
76 | go hub.Run(ctx, setup.ClientListener)
77 |
78 | time.Sleep(time.Second)
79 |
80 | a, err := agent.NewAgent(L)
81 | require.NoError(t, err)
82 |
83 | a.Token = setup.AgentToken
84 |
85 | var fe fakeHTTPService
86 |
87 | _, err = a.AddService(&agent.Service{
88 | Type: "http",
89 | Labels: pb.ParseLabelSet("env=test1,:deployment=aabbcc"),
90 | Handler: &fe,
91 | })
92 | require.NoError(t, err)
93 |
94 | err = a.Start(ctx, discovery.HubConfigs(discovery.HubConfig{
95 | Addr: setup.HubAddr,
96 | Insecure: true,
97 | }))
98 | require.NoError(t, err)
99 |
100 | go a.Wait(ctx)
101 |
102 | time.Sleep(time.Second)
103 |
104 | name := "fuzz.localdomain"
105 |
106 | _, err = setup.ControlServer.AddLabelLink(setup.MgmtCtx,
107 | &pb.AddLabelLinkRequest{
108 | Labels: pb.ParseLabelSet(":hostname=" + name),
109 | Account: setup.Account,
110 | Target: pb.ParseLabelSet("env=test1"),
111 | })
112 |
113 | require.NoError(t, err)
114 |
115 | time.Sleep(time.Second)
116 |
117 | require.NoError(t, setup.ControlClient.ForceLabelLinkUpdate(ctx, L))
118 |
119 | t.Run("routes requests to the agent", func(t *testing.T) {
120 | f, err := web.NewFrontend(L, hub, setup.ControlClient, setup.HubServToken)
121 | require.NoError(t, err)
122 |
123 | req, err := http.NewRequest("GET", "http://"+name+"/", strings.NewReader("this is a request"))
124 | require.NoError(t, err)
125 |
126 | w := httptest.NewRecorder()
127 |
128 | t.Log("sending the request")
129 | f.ServeHTTP(w, req)
130 |
131 | assert.Equal(t, 247, w.Code)
132 | expected := "this is from the fake service: this is a request"
133 | assert.Equal(t, expected, w.Body.String())
134 |
135 | assert.Equal(t, "fuzz.localdomain", fe.host)
136 | })
137 |
138 | t.Run("supports deployment routes", func(t *testing.T) {
139 | target := "fuzz--aabbcc.localdomain"
140 |
141 | f, err := web.NewFrontend(L, hub, setup.ControlClient, setup.HubServToken)
142 | require.NoError(t, err)
143 |
144 | req, err := http.NewRequest("GET", "http://"+target+"/", strings.NewReader("this is a request"))
145 | require.NoError(t, err)
146 |
147 | w := httptest.NewRecorder()
148 |
149 | f.ServeHTTP(w, req)
150 |
151 | assert.Equal(t, 247, w.Code)
152 | expected := "this is from the fake service: this is a request"
153 | assert.Equal(t, expected, w.Body.String())
154 | })
155 | })
156 | }
157 |
--------------------------------------------------------------------------------
/pkg/connect/connect.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package connect
5 |
6 | import (
7 | "crypto/tls"
8 | "errors"
9 | "net"
10 | "sync/atomic"
11 | "time"
12 |
13 | "github.com/armon/go-metrics"
14 | "github.com/hashicorp/go-hclog"
15 | "github.com/hashicorp/horizon/pkg/pb"
16 | "github.com/hashicorp/horizon/pkg/wire"
17 | "github.com/hashicorp/yamux"
18 | )
19 |
20 | type Session struct {
21 | conn net.Conn
22 | session *yamux.Session
23 | }
24 |
25 | type Conn struct {
26 | serviceId *pb.ULID
27 | fr *wire.FramingReader
28 | fw *wire.FramingWriter
29 | }
30 |
31 | var ErrInvalidToken = errors.New("invalid token")
32 |
33 | // We're trying this because in a busy system it could be a limiting
34 | // factor so having it pre-tracked will be useful.
35 | // TODO(emp): expose this via expvar or something like that.
36 | var activeSessions = new(int64)
37 |
38 | func Connect(L hclog.Logger, addr, token string) (*Session, error) {
39 | var clientTlsConfig tls.Config
40 | clientTlsConfig.InsecureSkipVerify = true
41 | clientTlsConfig.NextProtos = []string{"hzn"}
42 |
43 | cconn, err := tls.Dial("tcp", addr, &clientTlsConfig)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | var preamble pb.Preamble
49 | preamble.Token = token
50 |
51 | fw, err := wire.NewFramingWriter(cconn)
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | _, err = fw.WriteMarshal(1, &preamble)
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | fr, err := wire.NewFramingReader(cconn)
62 | if err != nil {
63 | return nil, err
64 | }
65 |
66 | var confirmation pb.Confirmation
67 |
68 | _, _, err = fr.ReadMarshal(&confirmation)
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | if confirmation.Status != "connected" {
74 | return nil, ErrInvalidToken
75 | }
76 |
77 | bc := &wire.ComposedConn{
78 | Reader: fr.BufReader(),
79 | Writer: cconn,
80 | Closer: cconn,
81 | }
82 |
83 | cfg := yamux.DefaultConfig()
84 | cfg.EnableKeepAlive = true
85 | cfg.KeepAliveInterval = 30 * time.Second
86 | cfg.Logger = L.StandardLogger(&hclog.StandardLoggerOptions{
87 | InferLevels: true,
88 | })
89 | cfg.LogOutput = nil
90 |
91 | session, err := yamux.Client(bc, cfg)
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | val := atomic.AddInt64(activeSessions, 1)
97 | metrics.SetGauge([]string{"connect", "sessions"}, float32(val))
98 |
99 | return &Session{conn: cconn, session: session}, nil
100 | }
101 |
102 | func (s *Session) Close() error {
103 | val := atomic.AddInt64(activeSessions, -1)
104 | metrics.SetGauge([]string{"connect", "sessions"}, float32(val))
105 |
106 | s.session.Close()
107 | return s.conn.Close()
108 | }
109 |
110 | func (s *Session) ConnecToAccountService(acc *pb.Account, labels *pb.LabelSet) (*Conn, error) {
111 | stream, err := s.session.OpenStream()
112 | if err != nil {
113 | return nil, err
114 | }
115 |
116 | fr2, err := wire.NewFramingReader(stream)
117 | if err != nil {
118 | return nil, err
119 | }
120 |
121 | fw2, err := wire.NewFramingWriter(stream)
122 | if err != nil {
123 | return nil, err
124 | }
125 |
126 | var conreq pb.ConnectRequest
127 | conreq.Target = labels
128 | conreq.PivotAccount = acc
129 |
130 | _, err = fw2.WriteMarshal(1, &conreq)
131 | if err != nil {
132 | return nil, err
133 | }
134 |
135 | var ack pb.ConnectAck
136 |
137 | tag, _, err := fr2.ReadMarshal(&ack)
138 | if err != nil {
139 | return nil, err
140 | }
141 |
142 | if tag != 1 {
143 | return nil, wire.ErrProtocolError
144 | }
145 |
146 | return &Conn{serviceId: ack.ServiceId, fr: fr2, fw: fw2}, nil
147 | }
148 |
149 | func (s *Session) ConnectToService(labels *pb.LabelSet) (*Conn, error) {
150 | return s.ConnecToAccountService(nil, labels)
151 | }
152 |
153 | func (c *Conn) ReadMarshal(v wire.Unmarshaller) (byte, error) {
154 | tag, _, err := c.fr.ReadMarshal(v)
155 | if err != nil {
156 | return 0, err
157 | }
158 |
159 | return tag, nil
160 | }
161 |
162 | func (c *Conn) WriteMarshal(tag byte, v wire.Marshaller) error {
163 | _, err := c.fw.WriteMarshal(tag, v)
164 | return err
165 | }
166 |
167 | func (c *Conn) WireContext(accountId *pb.Account) wire.Context {
168 | return wire.NewContext(accountId, c.fr, c.fw)
169 | }
170 |
171 | func (c *Conn) ServiceId() *pb.ULID {
172 | return c.serviceId
173 | }
174 |
--------------------------------------------------------------------------------
/cmd/hznagent/pipe.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package main
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "io"
10 | "log"
11 | "os"
12 |
13 | "github.com/hashicorp/go-hclog"
14 | "github.com/hashicorp/horizon/pkg/agent"
15 | "github.com/hashicorp/horizon/pkg/discovery"
16 | "github.com/hashicorp/horizon/pkg/pb"
17 | "github.com/spf13/pflag"
18 | )
19 |
20 | type pipeRunner struct {
21 | flags *pflag.FlagSet
22 | fControl *string
23 | fHub *string
24 | fToken *string
25 | fId *string
26 | fListen *bool
27 | fVerbose *int
28 | }
29 |
30 | func (c *pipeRunner) init() error {
31 | c.flags = pflag.NewFlagSet("agent", pflag.ExitOnError)
32 | c.fControl = c.flags.String("control", "control.alpha.hzn.network", "address of control plane")
33 | c.fHub = c.flags.String("hub", "", "address of a hub to connect to")
34 | c.fToken = c.flags.String("token", "", "authentication token")
35 | c.fId = c.flags.String("id", "", "pipe identifier")
36 | c.fListen = c.flags.Bool("listen", false, "listen for a pipe connection")
37 | c.fVerbose = c.flags.CountP("verbose", "v", "increase verbosity of output")
38 | return nil
39 | }
40 |
41 | func (c *pipeRunner) Help() string {
42 | str := "horizon pipe:"
43 | str += c.flags.FlagUsagesWrapped(4)
44 | return str
45 | }
46 |
47 | type pipeHandler struct {
48 | cancel func()
49 | }
50 |
51 | func (p *pipeHandler) HandleRequest(ctx context.Context, L hclog.Logger, sctx agent.ServiceContext) error {
52 | defer p.cancel()
53 | defer sctx.Close()
54 |
55 | r := sctx.Reader()
56 | w := sctx.Writer()
57 |
58 | defer w.Close()
59 |
60 | go func() {
61 | defer w.Close()
62 | io.Copy(w, os.Stdin)
63 | }()
64 |
65 | io.Copy(os.Stdout, r)
66 |
67 | return nil
68 | }
69 |
70 | func (c *pipeRunner) Run(args []string) int {
71 | c.flags.Parse(args)
72 |
73 | level := hclog.Warn
74 |
75 | switch *c.fVerbose {
76 | case 1:
77 | level = hclog.Info
78 | case 2:
79 | level = hclog.Debug
80 | case 3:
81 | level = hclog.Trace
82 | }
83 |
84 | L := hclog.New(&hclog.LoggerOptions{
85 | Name: "hznagent",
86 | Level: level,
87 | })
88 |
89 | ctx := context.Background()
90 |
91 | var config discovery.HubConfigProvider
92 |
93 | if c.fHub != nil {
94 | L.Debug("using static hub config")
95 |
96 | config = discovery.HubConfigs(discovery.HubConfig{
97 | Name: "cli",
98 | Addr: *c.fHub,
99 | Insecure: true,
100 | })
101 | } else {
102 | L.Debug("discovering hub config")
103 |
104 | dc, err := discovery.NewClient(*c.fControl)
105 | if err != nil {
106 | log.Fatal(err)
107 | }
108 |
109 | L.Debug("refreshing data")
110 |
111 | err = dc.Refresh(ctx)
112 | if err != nil {
113 | log.Fatal(err)
114 | }
115 |
116 | config = dc
117 | }
118 |
119 | L.Debug("starting agent")
120 |
121 | g, err := agent.NewAgent(L.Named("agent"))
122 | if err != nil {
123 | log.Fatal(err)
124 | }
125 |
126 | g.Token = Token(c.fToken)
127 |
128 | if *c.fId == "" {
129 | *c.fId = pb.NewULID().SpecString()
130 | }
131 |
132 | labels := pb.MakeLabels("type", "pipe", "pipe-id", *c.fId)
133 |
134 | ctx, cancel := context.WithCancel(ctx)
135 | defer cancel()
136 |
137 | if *c.fListen {
138 | var ph pipeHandler
139 | ph.cancel = cancel
140 |
141 | _, err = g.AddService(&agent.Service{
142 | Type: "pipe",
143 | Labels: labels,
144 | Handler: &ph,
145 | })
146 |
147 | if err != nil {
148 | log.Fatal(err)
149 | }
150 | }
151 |
152 | err = g.Start(ctx, config)
153 | if err != nil {
154 | log.Fatal(err)
155 | }
156 |
157 | fmt.Fprintf(os.Stderr, "Identifier: %s", *c.fId)
158 |
159 | L.Info("pipe identifier", "id", *c.fId, "labels", labels)
160 |
161 | if !*c.fListen {
162 | go func() {
163 | defer cancel()
164 | rc, err := g.Connect(labels)
165 | if err != nil {
166 | L.Error("error connecting to service", "error", err)
167 | return
168 | }
169 |
170 | defer rc.Close()
171 |
172 | go func() {
173 | defer rc.Close()
174 | io.Copy(rc, os.Stdin)
175 | }()
176 |
177 | io.Copy(os.Stdout, rc)
178 | }()
179 | }
180 |
181 | L.Debug("agent running")
182 | err = g.Wait(ctx)
183 | if err != nil {
184 | log.Fatal(err)
185 | }
186 |
187 | return 0
188 | }
189 |
190 | func (c *pipeRunner) Synopsis() string {
191 | return "proxy to a TCP server provided by a horizon service"
192 | }
193 |
--------------------------------------------------------------------------------
/pkg/token/creator.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "bytes"
8 | "crypto/ed25519"
9 | "encoding/base64"
10 | "fmt"
11 | "path/filepath"
12 | "time"
13 |
14 | "github.com/hashicorp/horizon/pkg/pb"
15 | "github.com/hashicorp/vault/api"
16 | "golang.org/x/crypto/blake2b"
17 | )
18 |
19 | type TokenCreator struct {
20 | Role pb.TokenRole
21 | Issuer string
22 | AccountId *pb.ULID
23 | AccuntNamespace string
24 | Capabilities map[pb.Capability]string
25 | Metadata map[string]string
26 | ValidDuration time.Duration
27 |
28 | RawCapabilities []pb.TokenCapability
29 | }
30 |
31 | const (
32 | Magic = 0x47
33 | )
34 |
35 | func (c *TokenCreator) body() ([]byte, error) {
36 | capa := c.RawCapabilities
37 |
38 | for k, v := range c.Capabilities {
39 | capa = append(capa, pb.TokenCapability{
40 | Capability: k,
41 | Value: v,
42 | })
43 | }
44 |
45 | body := &pb.Token_Body{
46 | Role: c.Role,
47 | Id: pb.NewULID(),
48 | Account: &pb.Account{
49 | Namespace: c.AccuntNamespace,
50 | AccountId: c.AccountId,
51 | },
52 | Capabilities: capa,
53 | }
54 |
55 | if c.ValidDuration > 0 {
56 | body.ValidUntil = pb.NewTimestamp(time.Now().Add(c.ValidDuration))
57 | }
58 |
59 | return body.Marshal()
60 | }
61 |
62 | func (c *TokenCreator) EncodeHMAC(key []byte, keyId string) (string, error) {
63 | var t pb.Token
64 |
65 | t.Metadata = &pb.Headers{}
66 |
67 | for k, v := range c.Metadata {
68 | t.Metadata.Headers = append(t.Metadata.Headers, &pb.KVPair{
69 | Key: k,
70 | Value: v,
71 | })
72 | }
73 |
74 | data, err := c.body()
75 | if err != nil {
76 | return "", err
77 | }
78 |
79 | t.Body = data
80 |
81 | h, err := blake2b.New256(key)
82 | if err != nil {
83 | return "", err
84 | }
85 |
86 | h.Write(data)
87 |
88 | t.Signatures = []*pb.Signature{
89 | {
90 | SigType: pb.BLAKE2HMAC,
91 | KeyId: keyId,
92 | Signature: h.Sum(nil),
93 | },
94 | }
95 |
96 | tdata, err := t.Marshal()
97 | if err != nil {
98 | return "", err
99 | }
100 |
101 | var buf bytes.Buffer
102 |
103 | buf.WriteByte(Magic)
104 | buf.Write(tdata)
105 |
106 | return Armor(buf.Bytes()), nil
107 | }
108 |
109 | func (c *TokenCreator) EncodeED25519(key ed25519.PrivateKey, keyId string) (string, error) {
110 | var t pb.Token
111 |
112 | t.Metadata = &pb.Headers{}
113 |
114 | for k, v := range c.Metadata {
115 | t.Metadata.Headers = append(t.Metadata.Headers, &pb.KVPair{
116 | Key: k,
117 | Value: v,
118 | })
119 | }
120 |
121 | data, err := c.body()
122 | if err != nil {
123 | return "", err
124 | }
125 |
126 | t.Body = data
127 |
128 | t.Signatures = []*pb.Signature{
129 | {
130 | SigType: pb.ED25519,
131 | KeyId: keyId,
132 | Signature: ed25519.Sign(key, data),
133 | },
134 | }
135 |
136 | tdata, err := t.Marshal()
137 | if err != nil {
138 | return "", err
139 | }
140 |
141 | var buf bytes.Buffer
142 |
143 | buf.WriteByte(Magic)
144 | buf.Write(tdata)
145 |
146 | return Armor(buf.Bytes()), nil
147 | }
148 |
149 | func (c *TokenCreator) EncodeED25519WithVault(vc *api.Client, path, keyId string) (string, error) {
150 | var t pb.Token
151 |
152 | t.Metadata = &pb.Headers{}
153 |
154 | for k, v := range c.Metadata {
155 | t.Metadata.Headers = append(t.Metadata.Headers, &pb.KVPair{
156 | Key: k,
157 | Value: v,
158 | })
159 | }
160 |
161 | data, err := c.body()
162 | if err != nil {
163 | return "", err
164 | }
165 |
166 | secret, err := vc.Logical().Write(filepath.Join("/transit/sign", path), map[string]interface{}{
167 | "input": base64.StdEncoding.EncodeToString(data),
168 | "marshaling_algorithm": "jws",
169 | })
170 |
171 | if err != nil {
172 | return "", err
173 | }
174 |
175 | ct, ok := secret.Data["signature"].(string)
176 | if !ok {
177 | return "", fmt.Errorf("vault response missing ciphertext")
178 | }
179 |
180 | sig, err := base64.RawURLEncoding.DecodeString(ct[9:])
181 | if err != nil {
182 | return "", err
183 | }
184 |
185 | t.Body = data
186 |
187 | t.Signatures = []*pb.Signature{
188 | {
189 | SigType: pb.ED25519,
190 | KeyId: keyId,
191 | Signature: sig,
192 | },
193 | }
194 |
195 | tdata, err := t.Marshal()
196 | if err != nil {
197 | return "", err
198 | }
199 |
200 | var buf bytes.Buffer
201 |
202 | buf.WriteByte(Magic)
203 | buf.Write(tdata)
204 |
205 | return Armor(buf.Bytes()), nil
206 | }
207 |
--------------------------------------------------------------------------------
/pkg/agent/agent_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package agent
5 |
6 | import (
7 | "context"
8 | "crypto/tls"
9 | "testing"
10 | "time"
11 |
12 | "github.com/hashicorp/go-hclog"
13 | "github.com/hashicorp/horizon/pkg/control"
14 | "github.com/hashicorp/horizon/pkg/dbx"
15 | "github.com/hashicorp/horizon/pkg/discovery"
16 | "github.com/hashicorp/horizon/pkg/hub"
17 | "github.com/hashicorp/horizon/pkg/pb"
18 | "github.com/hashicorp/horizon/pkg/testutils/central"
19 | "github.com/hashicorp/horizon/pkg/wire"
20 | "github.com/hashicorp/yamux"
21 | "github.com/stretchr/testify/assert"
22 | "github.com/stretchr/testify/require"
23 | )
24 |
25 | func TestAgent(t *testing.T) {
26 | t.Run("can connect and have a service connected to", func(t *testing.T) {
27 | central.Dev(t, func(setup *central.DevSetup) {
28 |
29 | L := hclog.New(&hclog.LoggerOptions{
30 | Name: "dev",
31 | Level: hclog.Trace,
32 | })
33 |
34 | h, err := hub.NewHub(L.Named("hub"), setup.ControlClient, setup.HubServToken)
35 | require.NoError(t, err)
36 |
37 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
38 | defer cancel()
39 |
40 | go func() {
41 | err := h.Run(ctx, setup.ClientListener)
42 | require.NoError(t, err)
43 | }()
44 |
45 | time.Sleep(time.Second)
46 |
47 | agent, err := NewAgent(L.Named("agent"))
48 | require.NoError(t, err)
49 |
50 | agent.Token = setup.AgentToken
51 |
52 | serviceId, err := agent.AddService(&Service{
53 | Type: "test",
54 | Labels: pb.ParseLabelSet("env=test,service=echo"),
55 | Handler: EchoHandler(),
56 | })
57 |
58 | require.NoError(t, err)
59 |
60 | err = agent.Start(ctx, discovery.HubConfigs(discovery.HubConfig{
61 | Addr: setup.HubAddr,
62 | Insecure: true,
63 | }))
64 | require.NoError(t, err)
65 |
66 | go agent.Wait(ctx)
67 |
68 | time.Sleep(time.Second)
69 |
70 | var so control.Service
71 | err = dbx.Check(setup.DB.First(&so))
72 |
73 | assert.NoError(t, err)
74 |
75 | assert.Equal(t, serviceId.Bytes(), so.ServiceId)
76 |
77 | // Ok, now try to connect to the service via the hub
78 |
79 | var clientTlsConfig tls.Config
80 | clientTlsConfig.InsecureSkipVerify = true
81 | clientTlsConfig.NextProtos = []string{"hzn"}
82 |
83 | cconn, err := tls.Dial("tcp", setup.HubAddr, &clientTlsConfig)
84 | require.NoError(t, err)
85 |
86 | var preamble pb.Preamble
87 | preamble.Token = setup.AgentToken
88 |
89 | fw, err := wire.NewFramingWriter(cconn)
90 | require.NoError(t, err)
91 |
92 | _, err = fw.WriteMarshal(1, &preamble)
93 | require.NoError(t, err)
94 |
95 | start := time.Now()
96 |
97 | fr, err := wire.NewFramingReader(cconn)
98 | require.NoError(t, err)
99 |
100 | var confirmation pb.Confirmation
101 |
102 | tag, _, err := fr.ReadMarshal(&confirmation)
103 | require.NoError(t, err)
104 |
105 | assert.Equal(t, uint8(1), tag)
106 |
107 | t.Logf("skew: %s", confirmation.Time.Time().Sub(start))
108 |
109 | assert.Equal(t, "connected", confirmation.Status)
110 |
111 | bc := &wire.ComposedConn{
112 | Reader: fr.BufReader(),
113 | Writer: cconn,
114 | Closer: cconn,
115 | }
116 |
117 | cfg := yamux.DefaultConfig()
118 | cfg.EnableKeepAlive = true
119 | cfg.KeepAliveInterval = 30 * time.Second
120 | cfg.Logger = L.StandardLogger(&hclog.StandardLoggerOptions{
121 | InferLevels: true,
122 | })
123 | cfg.LogOutput = nil
124 |
125 | session, err := yamux.Client(bc, cfg)
126 | require.NoError(t, err)
127 |
128 | stream, err := session.OpenStream()
129 | require.NoError(t, err)
130 |
131 | fr2, err := wire.NewFramingReader(stream)
132 | require.NoError(t, err)
133 |
134 | fw2, err := wire.NewFramingWriter(stream)
135 | require.NoError(t, err)
136 |
137 | var conreq pb.ConnectRequest
138 | conreq.Target = pb.ParseLabelSet("service=echo")
139 |
140 | _, err = fw2.WriteMarshal(1, &conreq)
141 | require.NoError(t, err)
142 |
143 | var ack pb.ConnectAck
144 |
145 | tag, _, err = fr2.ReadMarshal(&ack)
146 | require.NoError(t, err)
147 |
148 | assert.Equal(t, uint8(1), tag)
149 |
150 | assert.Equal(t, serviceId, ack.ServiceId)
151 |
152 | mb := wire.MarshalBytes("hello hzn")
153 |
154 | _, err = fw2.WriteMarshal(30, &mb)
155 | require.NoError(t, err)
156 |
157 | var mb2 wire.MarshalBytes
158 |
159 | tag, _, err = fr2.ReadMarshal(&mb2)
160 | require.NoError(t, err)
161 |
162 | assert.Equal(t, uint8(30), tag)
163 |
164 | assert.Equal(t, []byte("hello hzn"), []byte(mb2))
165 | })
166 | })
167 | }
168 |
--------------------------------------------------------------------------------
/pkg/token/token_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package token
5 |
6 | import (
7 | "crypto/ed25519"
8 | "crypto/rand"
9 | "errors"
10 | "testing"
11 | "time"
12 |
13 | "github.com/hashicorp/horizon/pkg/pb"
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestToken(t *testing.T) {
19 | t.Run("create and validate a token", func(t *testing.T) {
20 | var tc TokenCreator
21 | tc.AccountId = pb.NewULID()
22 | tc.AccuntNamespace = "/test"
23 | tc.Capabilities = map[pb.Capability]string{
24 | pb.CONNECT: "",
25 | pb.SERVE: "",
26 | }
27 |
28 | pub, key, err := ed25519.GenerateKey(rand.Reader)
29 | require.NoError(t, err)
30 |
31 | stoken, err := tc.EncodeED25519(key, "k1")
32 | require.NoError(t, err)
33 |
34 | vt, err := CheckTokenED25519(stoken, pub)
35 | require.NoError(t, err)
36 |
37 | cb := func(ok bool, _ string) bool {
38 | return ok
39 | }
40 |
41 | assert.True(t, cb(vt.HasCapability(pb.CONNECT)))
42 | assert.True(t, cb(vt.HasCapability(pb.SERVE)))
43 | assert.False(t, cb(vt.HasCapability(pb.ACCESS)))
44 | assert.Equal(t, "k1", vt.KeyId)
45 | })
46 |
47 | t.Run("detect alterations", func(t *testing.T) {
48 | var tc TokenCreator
49 | tc.AccountId = pb.NewULID()
50 | tc.AccuntNamespace = "/test"
51 | tc.Capabilities = map[pb.Capability]string{
52 | pb.CONNECT: "",
53 | pb.SERVE: "",
54 | }
55 |
56 | pub, key, err := ed25519.GenerateKey(rand.Reader)
57 | require.NoError(t, err)
58 |
59 | stoken, err := tc.EncodeED25519(key, "k1")
60 | require.NoError(t, err)
61 |
62 | token, err := RemoveArmor(stoken)
63 | require.NoError(t, err)
64 |
65 | var tkn pb.Token
66 | err = tkn.Unmarshal(token[1:])
67 | require.NoError(t, err)
68 |
69 | var body pb.Token_Body
70 | err = body.Unmarshal(tkn.Body)
71 | require.NoError(t, err)
72 |
73 | body.Capabilities = append(body.Capabilities, pb.TokenCapability{
74 | Capability: pb.ACCESS,
75 | Value: "/",
76 | })
77 |
78 | data, err := body.Marshal()
79 | require.NoError(t, err)
80 |
81 | tkn.Body = data
82 |
83 | evilToken, err := tkn.Marshal()
84 | require.NoError(t, err)
85 |
86 | stoken = Armor(append(token[:1], evilToken...))
87 |
88 | _, err = CheckTokenED25519(stoken, pub)
89 | require.Error(t, err)
90 | assert.True(t, errors.Is(err, ErrBadToken))
91 | })
92 |
93 | t.Run("detect wrong key", func(t *testing.T) {
94 | var tc TokenCreator
95 | tc.AccountId = pb.NewULID()
96 | tc.AccuntNamespace = "/test"
97 | tc.Capabilities = map[pb.Capability]string{
98 | pb.CONNECT: "",
99 | pb.SERVE: "",
100 | }
101 |
102 | _, key, err := ed25519.GenerateKey(rand.Reader)
103 | require.NoError(t, err)
104 |
105 | stoken, err := tc.EncodeED25519(key, "k1")
106 | require.NoError(t, err)
107 |
108 | pub, _, err := ed25519.GenerateKey(rand.Reader)
109 | require.NoError(t, err)
110 |
111 | _, err = CheckTokenED25519(stoken, pub)
112 | require.Error(t, err)
113 | assert.True(t, errors.Is(err, ErrBadToken))
114 | })
115 |
116 | t.Run("checks the tokens is before the end of the time window", func(t *testing.T) {
117 | var tc TokenCreator
118 | tc.AccountId = pb.NewULID()
119 | tc.AccuntNamespace = "/test"
120 | tc.Capabilities = map[pb.Capability]string{
121 | pb.CONNECT: "",
122 | pb.SERVE: "",
123 | }
124 | tc.ValidDuration = time.Second
125 |
126 | pub, key, err := ed25519.GenerateKey(rand.Reader)
127 | require.NoError(t, err)
128 |
129 | stoken, err := tc.EncodeED25519(key, "k1")
130 | require.NoError(t, err)
131 |
132 | time.Sleep(time.Second)
133 |
134 | _, err = CheckTokenED25519(stoken, pub)
135 | require.Error(t, err)
136 | assert.True(t, errors.Is(err, ErrNoLongerValid))
137 | })
138 |
139 | t.Run("checks the tokens is after the beginning of the time window", func(t *testing.T) {
140 | n := timeNow
141 | defer func() {
142 | timeNow = n
143 | }()
144 |
145 | timeNow = func() time.Time {
146 | return time.Now().Add(-10 * time.Minute)
147 | }
148 |
149 | var tc TokenCreator
150 | tc.AccountId = pb.NewULID()
151 | tc.AccuntNamespace = "/test"
152 | tc.Capabilities = map[pb.Capability]string{
153 | pb.CONNECT: "",
154 | pb.SERVE: "",
155 | }
156 |
157 | pub, key, err := ed25519.GenerateKey(rand.Reader)
158 | require.NoError(t, err)
159 |
160 | stoken, err := tc.EncodeED25519(key, "k1")
161 | require.NoError(t, err)
162 |
163 | time.Sleep(time.Second)
164 |
165 | _, err = CheckTokenED25519(stoken, pub)
166 | require.Error(t, err)
167 | assert.True(t, errors.Is(err, ErrNoLongerValid))
168 | })
169 |
170 | }
171 |
--------------------------------------------------------------------------------
/pkg/netloc/best.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package netloc
5 |
6 | import (
7 | "context"
8 | "sort"
9 | "time"
10 |
11 | "github.com/hashicorp/go-hclog"
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | )
14 |
15 | type BestInput struct {
16 | Count int
17 | Local []*pb.NetworkLocation
18 | Remote []*pb.NetworkLocation
19 | PublicOnly bool
20 |
21 | Latency func(addr string) error
22 | }
23 |
24 | // Given 2 sets of network locations, return the best N (count) ones that local should use
25 | // to connect to remote
26 | func FindBestLive(ctx context.Context, input *BestInput, locs chan *pb.NetworkLocation) error {
27 | defer close(locs)
28 |
29 | var best []*pb.NetworkLocation
30 |
31 | if input.PublicOnly {
32 | for _, loc := range input.Remote {
33 | if !loc.Labels.Contains("type", "private") {
34 | best = append(best, loc)
35 | }
36 | }
37 | } else {
38 | best = append(best, input.Remote...)
39 | }
40 |
41 | cards := make([]int, len(best))
42 |
43 | for i, rloc := range best {
44 | card := 0
45 |
46 | for _, lloc := range input.Local {
47 | c := lloc.Cardinality(rloc)
48 | if c > card {
49 | card = c
50 | }
51 | }
52 |
53 | cards[i] = card
54 | }
55 |
56 | sort.Slice(best, func(i, j int) bool {
57 | // j and i are flipped here so the results are sorted desc rather than asc
58 | return cards[j] < cards[i]
59 | })
60 |
61 | if input.Latency == nil {
62 | if input.Count != 0 && input.Count > len(best) {
63 | best = best[:input.Count]
64 | }
65 |
66 | for _, loc := range best {
67 | select {
68 | case <-ctx.Done():
69 | return ctx.Err()
70 | case locs <- loc:
71 | // ok
72 | }
73 | }
74 |
75 | return nil
76 | }
77 |
78 | if input.Latency != nil {
79 | type result struct {
80 | ok bool
81 | pos int
82 | }
83 |
84 | results := make(chan result)
85 |
86 | for i, loc := range best {
87 | go func(i int, loc *pb.NetworkLocation) {
88 | addr := loc.Addresses[0]
89 | err := input.Latency(addr)
90 |
91 | var ok bool
92 |
93 | if err == nil {
94 | ok = true
95 | }
96 |
97 | results <- result{
98 | ok: ok,
99 | pos: i,
100 | }
101 | }(i, loc)
102 | }
103 |
104 | for i := 0; i < len(best); i++ {
105 | res := <-results
106 | if input.Count == 0 || i < input.Count {
107 | if res.ok {
108 | select {
109 | case <-ctx.Done():
110 | return ctx.Err()
111 | case locs <- best[res.pos]:
112 | // ok
113 | }
114 | }
115 | }
116 | }
117 | }
118 |
119 | return nil
120 | }
121 |
122 | // Given 2 sets of network locations, return the best N (count) ones that local should use
123 | // to connect to remote
124 | func FindBest(input *BestInput) ([]*pb.NetworkLocation, error) {
125 | var best []*pb.NetworkLocation
126 |
127 | if input.PublicOnly {
128 | for _, loc := range input.Remote {
129 | if !loc.Labels.Contains("type", "private") {
130 | best = append(best, loc)
131 | }
132 | }
133 | } else {
134 | best = append(best, input.Remote...)
135 | }
136 |
137 | cards := make([]int, len(best))
138 |
139 | for i, rloc := range best {
140 | card := 0
141 |
142 | for _, lloc := range input.Local {
143 | c := lloc.Cardinality(rloc)
144 | if c > card {
145 | card = c
146 | }
147 | }
148 |
149 | cards[i] = card
150 | }
151 |
152 | sort.Slice(best, func(i, j int) bool {
153 | // j and i are flipped here so the results are sorted desc rather than asc
154 | return cards[j] < cards[i]
155 | })
156 |
157 | if input.Latency != nil {
158 | latency := make([]time.Duration, len(best))
159 | available := make([]bool, len(best))
160 |
161 | type result struct {
162 | latency time.Duration
163 | ok bool
164 | pos int
165 | }
166 |
167 | results := make(chan result)
168 |
169 | for i, loc := range best {
170 | go func(i int, loc *pb.NetworkLocation) {
171 | t := time.Now()
172 | addr := loc.Addresses[0]
173 | err := input.Latency(addr)
174 | latency := time.Since(t)
175 |
176 | hclog.L().Info("latency", "addr", addr, "latency", latency)
177 |
178 | var ok bool
179 |
180 | if err == nil {
181 | ok = true
182 | }
183 |
184 | results <- result{
185 | latency: latency,
186 | ok: ok,
187 | pos: i,
188 | }
189 | }(i, loc)
190 | }
191 |
192 | for range latency {
193 | res := <-results
194 | latency[res.pos] = res.latency
195 | available[res.pos] = res.ok
196 | }
197 |
198 | var prune []*pb.NetworkLocation
199 |
200 | for i, loc := range best {
201 | if available[i] {
202 | prune = append(prune, loc)
203 | }
204 | }
205 |
206 | best = prune
207 |
208 | sort.Slice(best, func(i, j int) bool {
209 | return latency[i] < latency[j]
210 | })
211 | }
212 |
213 | if len(best) > input.Count {
214 | best = best[:input.Count]
215 | }
216 |
217 | return best, nil
218 | }
219 |
--------------------------------------------------------------------------------
/pkg/wire/framing.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 |
4 | package wire
5 |
6 | import (
7 | "bufio"
8 | "encoding/binary"
9 | "io"
10 | "sync"
11 |
12 | "github.com/hashicorp/horizon/pkg/pb"
13 | "github.com/pkg/errors"
14 | )
15 |
16 | var frPool = sync.Pool{
17 | New: func() interface{} { return &FramingReader{} },
18 | }
19 |
20 | type FramingReader struct {
21 | br *bufio.Reader
22 |
23 | readLeft int
24 | }
25 |
26 | func NewFramingReader(r io.Reader) (*FramingReader, error) {
27 | vf := frPool.Get().(*FramingReader)
28 |
29 | if vf.br == nil {
30 | vf.br = bufio.NewReader(r)
31 | } else {
32 | vf.br.Reset(r)
33 | }
34 |
35 | return vf, nil
36 | }
37 |
38 | func (f *FramingReader) Recycle() {
39 | // frPool.Put(f)
40 | }
41 |
42 | func (f *FramingReader) BufReader() *bufio.Reader {
43 | return f.br
44 | }
45 |
46 | type recycleFR struct {
47 | io.Reader
48 | f *FramingReader
49 | }
50 |
51 | func (r *recycleFR) Recycle() {
52 | r.f.Recycle()
53 | }
54 |
55 | func (f *FramingReader) RecylableBufReader() io.Reader {
56 | return &recycleFR{Reader: f.br, f: f}
57 | }
58 |
59 | type Recyclable interface {
60 | Recycle()
61 | }
62 |
63 | func Recycle(v interface{}) {
64 | if r, ok := v.(Recyclable); ok {
65 | r.Recycle()
66 | }
67 | }
68 |
69 | func (f *FramingReader) Next() (byte, int, error) {
70 | tag, err := f.br.ReadByte()
71 | if err != nil {
72 | return 0, 0, err
73 | }
74 |
75 | sz, err := binary.ReadUvarint(f.br)
76 | if err != nil {
77 | return 0, 0, err
78 | }
79 |
80 | f.readLeft = int(sz)
81 |
82 | return tag, int(sz), nil
83 | }
84 |
85 | func (f *FramingReader) Read(b []byte) (int, error) {
86 | if f.readLeft == 0 {
87 | return 0, io.EOF
88 | }
89 |
90 | if f.readLeft > len(b) {
91 | f.readLeft -= len(b)
92 | } else {
93 | b = b[:f.readLeft]
94 | f.readLeft = 0
95 | }
96 |
97 | return f.br.Read(b)
98 | }
99 |
100 | var frameBufPool = sync.Pool{}
101 |
102 | type Unmarshaller interface {
103 | Unmarshal([]byte) error
104 | }
105 |
106 | var ErrRemoteError = errors.New("remote error detected")
107 |
108 | func (f *FramingReader) ReadMarshal(v Unmarshaller) (byte, int, error) {
109 | tag, sz, err := f.Next()
110 | if err != nil {
111 | return 0, 0, err
112 | }
113 |
114 | buf, ok := frameBufPool.Get().([]byte)
115 | if !ok || sz > len(buf) {
116 | buf = make([]byte, sz+128)
117 | }
118 |
119 | defer frameBufPool.Put(buf)
120 |
121 | _, err = io.ReadFull(f, buf[:sz])
122 | if err != nil {
123 | return 0, 0, err
124 | }
125 |
126 | if tag == 255 {
127 | var resp pb.Response
128 | resp.Unmarshal(buf[:sz])
129 | return 0, 0, errors.Wrapf(ErrRemoteError, resp.Error)
130 | }
131 |
132 | err = v.Unmarshal(buf[:sz])
133 | return tag, sz, err
134 | }
135 |
136 | func (f *FramingReader) ReadAdapter() *ReadAdapter {
137 | return &ReadAdapter{FR: f}
138 | }
139 |
140 | var fwPool = sync.Pool{
141 | New: func() interface{} {
142 | return &FramingWriter{
143 | sz: make([]byte, binary.MaxVarintLen64),
144 | }
145 | },
146 | }
147 |
148 | type flusher interface {
149 | Flush() error
150 | }
151 |
152 | type FramingWriter struct {
153 | bw *bufio.Writer
154 | flush flusher
155 |
156 | sz []byte
157 | writeLeft int
158 | }
159 |
160 | func NewFramingWriter(w io.Writer) (*FramingWriter, error) {
161 | fw := fwPool.Get().(*FramingWriter)
162 |
163 | if fw.bw == nil {
164 | fw.bw = bufio.NewWriter(w)
165 | } else {
166 | fw.bw.Reset(w)
167 | }
168 |
169 | if f, ok := w.(flusher); ok {
170 | fw.flush = f
171 | } else {
172 | fw.flush = nil
173 | }
174 |
175 | return fw, nil
176 | }
177 |
178 | func (f *FramingWriter) Recycle() {
179 | // fwPool.Put(f)
180 | }
181 |
182 | func (f *FramingWriter) WriteFrame(tag byte, size int) error {
183 | bufsz := binary.PutUvarint(f.sz, uint64(size))
184 |
185 | b := f.sz[:bufsz]
186 |
187 | f.bw.WriteByte(tag)
188 | f.bw.Write(b)
189 |
190 | if size == 0 {
191 | f.bw.Flush()
192 | if f.flush != nil {
193 | f.flush.Flush()
194 | }
195 | } else {
196 | f.writeLeft = size
197 | }
198 |
199 | return nil
200 | }
201 |
202 | var ErrTooMuchData = errors.New("more data that expected passed in")
203 |
204 | func (f *FramingWriter) Write(b []byte) (int, error) {
205 | if f.writeLeft == 0 {
206 | return 0, io.EOF
207 | }
208 |
209 | if len(b) > f.writeLeft {
210 | return 0, ErrTooMuchData
211 | }
212 |
213 | n, err := f.bw.Write(b)
214 | if err != nil {
215 | return n, err
216 | }
217 |
218 | f.writeLeft -= n
219 |
220 | if f.writeLeft == 0 {
221 | err = f.bw.Flush()
222 | if f.flush != nil {
223 | f.flush.Flush()
224 | }
225 | }
226 |
227 | return n, err
228 | }
229 |
230 | type Marshaller interface {
231 | Size() int
232 | MarshalTo(b []byte) (int, error)
233 | }
234 |
235 | func (f *FramingWriter) WriteMarshal(tag byte, v Marshaller) (int, error) {
236 | sz := v.Size()
237 | buf, ok := frameBufPool.Get().([]byte)
238 | if !ok || sz > len(buf) {
239 | buf = make([]byte, sz+128)
240 | }
241 |
242 | defer frameBufPool.Put(buf)
243 |
244 | _, err := v.MarshalTo(buf[:sz])
245 | if err != nil {
246 | return 0, err
247 | }
248 |
249 | err = f.WriteFrame(tag, sz)
250 | if err != nil {
251 | return 0, err
252 | }
253 |
254 | return f.Write(buf[:sz])
255 | }
256 |
257 | func (f *FramingWriter) WriteAdapter() *WriteAdapter {
258 | return &WriteAdapter{FW: f}
259 | }
260 |
--------------------------------------------------------------------------------