├── .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 |
17 | 20 |
21 |
22 |
23 | Error Icon 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 |
17 | 20 |
21 |
22 |
23 | Error Icon 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 |
17 | 20 |
21 |
22 |

23 | Waypoint is a tool built by 24 | HashiCorp for building, deploying 25 | and releasing applications. 26 |

27 | 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 | 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 | --------------------------------------------------------------------------------