diff --git a/jobs/rep/spec b/jobs/rep/spec index 425c85841b..55fda1550b 100644 --- a/jobs/rep/spec +++ b/jobs/rep/spec @@ -221,6 +221,9 @@ properties: diego.executor.instance_identity_validity_period_in_hours: description: "Validity period for the generated instance identity certificate" default: 24 + diego.executor.spiffe_socket_dir: + description: "Host directory of the SPIFFE workload socket to bind-mount read-only into app containers (must equal spiffe-agent host_socket_dir). Empty disables." + default: "" diego.executor.max_log_lines_per_second: description: "EXPERIMENTAL: Maximum log lines allowed per second per app instance. Default value of 0 will disable rate limiting. Minimum recommended value is 100." default: 0 diff --git a/jobs/rep/templates/rep.json.erb b/jobs/rep/templates/rep.json.erb index 301b1e0a99..c8930158f8 100644 --- a/jobs/rep/templates/rep.json.erb +++ b/jobs/rep/templates/rep.json.erb @@ -218,6 +218,10 @@ end end + if_p("diego.executor.spiffe_socket_dir") do |spiffe_dir| + config[:spiffe_socket_dir] = spiffe_dir unless spiffe_dir.to_s.empty? + end + config[:time_format] = "rfc3339" if_p("containers.layering_mode") do |mode| diff --git a/jobs/rep/templates/setup_mounted_data_dirs.erb b/jobs/rep/templates/setup_mounted_data_dirs.erb index d1ad1f2880..4b00905906 100644 --- a/jobs/rep/templates/setup_mounted_data_dirs.erb +++ b/jobs/rep/templates/setup_mounted_data_dirs.erb @@ -95,3 +95,19 @@ proxy_config_dir=${garden_shared_dir}/proxy_config rm -rf "$proxy_config_dir" mkdir -p "$proxy_config_dir" chown -R vcap:vcap "$proxy_config_dir" + +# SPIFFE workload socket directory (shared with the spiffe-agent job). The +# executor bind-mounts this host directory read-only into every app container so +# workloads can reach the spiffe-agent's Workload API socket. App containers run +# as an unprivileged, user-namespaced uid that is neither the directory owner nor +# in its group, so the directory must be world-traversable (o+x) for a workload +# to reach the 0666 socket inside it. 0711 withholds read (o-r): containers can +# connect to the known socket path but cannot enumerate the directory. +<% if_p("diego.executor.spiffe_socket_dir") do |spiffe_socket_dir| %> +<% unless spiffe_socket_dir.to_s.empty? %> +spiffe_socket_dir="<%= spiffe_socket_dir %>" +mkdir -p "$spiffe_socket_dir" +chown vcap:vcap "$spiffe_socket_dir" +chmod 0711 "$spiffe_socket_dir" +<% end %> +<% end %> diff --git a/jobs/spiffe-agent/monit b/jobs/spiffe-agent/monit new file mode 100644 index 0000000000..dc81c46ec7 --- /dev/null +++ b/jobs/spiffe-agent/monit @@ -0,0 +1,5 @@ +check process spiffe-agent + with pidfile /var/vcap/sys/run/bpm/spiffe-agent/spiffe-agent.pid + start program "/var/vcap/jobs/bpm/bin/bpm start spiffe-agent" + stop program "/var/vcap/jobs/bpm/bin/bpm stop spiffe-agent" + group vcap diff --git a/jobs/spiffe-agent/spec b/jobs/spiffe-agent/spec new file mode 100644 index 0000000000..f7d9ab1340 --- /dev/null +++ b/jobs/spiffe-agent/spec @@ -0,0 +1,40 @@ +--- +name: spiffe-agent + +templates: + spiffe-agent.json.erb: config/spiffe-agent.json + bpm.yml.erb: config/bpm.yml + signer_ca.crt.erb: config/certs/signer/ca.crt + bbs_ca.crt.erb: config/certs/bbs/ca.crt + bbs_client.crt.erb: config/certs/bbs/client.crt + bbs_client.key.erb: config/certs/bbs/client.key + +packages: + - spiffe-agent + +properties: + spiffe_agent.trust_domain: + description: "REQUIRED: SPIFFE trust domain for issued identities (e.g. cf.internal)." + spiffe_agent.signer_url: + description: "REQUIRED: Base URL of the UAA SPIFFE signer endpoint." + spiffe_agent.signer_client_id: + description: "OAuth client id used to authenticate to the signer." + spiffe_agent.signer_client_secret: + description: "OAuth client secret used to authenticate to the signer." + spiffe_agent.signer_ca_cert: + description: "PEM-encoded CA certificate used to verify the signer TLS endpoint. If empty, system roots are used." + spiffe_agent.bbs.api_location: + description: "Address of the BBS server." + default: "bbs.service.cf.internal:8889" + spiffe_agent.bbs.ca_cert: + description: "REQUIRED: PEM-encoded CA certificate for the BBS server." + spiffe_agent.bbs.client_cert: + description: "REQUIRED: PEM-encoded client certificate for the BBS server." + spiffe_agent.bbs.client_key: + description: "REQUIRED: PEM-encoded client key for the BBS server." + spiffe_agent.host_socket_dir: + description: "Directory on the host (shared with workloads) where the workload API socket is created." + default: "/var/vcap/data/spiffe-agent/shared" + spiffe_agent.log_level: + description: "Log level (debug, info, error, fatal)." + default: "info" diff --git a/jobs/spiffe-agent/templates/bbs_ca.crt.erb b/jobs/spiffe-agent/templates/bbs_ca.crt.erb new file mode 100644 index 0000000000..c6693b6be3 --- /dev/null +++ b/jobs/spiffe-agent/templates/bbs_ca.crt.erb @@ -0,0 +1 @@ +<%= p("spiffe_agent.bbs.ca_cert") %> diff --git a/jobs/spiffe-agent/templates/bbs_client.crt.erb b/jobs/spiffe-agent/templates/bbs_client.crt.erb new file mode 100644 index 0000000000..511f81e35b --- /dev/null +++ b/jobs/spiffe-agent/templates/bbs_client.crt.erb @@ -0,0 +1 @@ +<%= p("spiffe_agent.bbs.client_cert") %> diff --git a/jobs/spiffe-agent/templates/bbs_client.key.erb b/jobs/spiffe-agent/templates/bbs_client.key.erb new file mode 100644 index 0000000000..26c5900e19 --- /dev/null +++ b/jobs/spiffe-agent/templates/bbs_client.key.erb @@ -0,0 +1 @@ +<%= p("spiffe_agent.bbs.client_key") %> diff --git a/jobs/spiffe-agent/templates/bpm.yml.erb b/jobs/spiffe-agent/templates/bpm.yml.erb new file mode 100644 index 0000000000..0e9ea17599 --- /dev/null +++ b/jobs/spiffe-agent/templates/bpm.yml.erb @@ -0,0 +1,14 @@ +--- +processes: +- name: spiffe-agent + executable: /var/vcap/packages/spiffe-agent/bin/spiffe-agent + args: + - -config=/var/vcap/jobs/spiffe-agent/config/spiffe-agent.json + limits: + open_files: 100000 + unsafe: + privileged: true + host_pid_namespace: true + additional_volumes: + - path: <%= p("spiffe_agent.host_socket_dir") %> + writable: true diff --git a/jobs/spiffe-agent/templates/signer_ca.crt.erb b/jobs/spiffe-agent/templates/signer_ca.crt.erb new file mode 100644 index 0000000000..67aba106a8 --- /dev/null +++ b/jobs/spiffe-agent/templates/signer_ca.crt.erb @@ -0,0 +1 @@ +<%= p("spiffe_agent.signer_ca_cert", "") %> diff --git a/jobs/spiffe-agent/templates/spiffe-agent.json.erb b/jobs/spiffe-agent/templates/spiffe-agent.json.erb new file mode 100644 index 0000000000..9aceb93866 --- /dev/null +++ b/jobs/spiffe-agent/templates/spiffe-agent.json.erb @@ -0,0 +1,21 @@ +<%= + conf_dir = "/var/vcap/jobs/spiffe-agent/config" + host_socket_dir = p("spiffe_agent.host_socket_dir") + + config = { + log_level: p("spiffe_agent.log_level"), + socket_path: "#{host_socket_dir}/workload.sock", + trust_domain: p("spiffe_agent.trust_domain"), + cell_id: spec.id, + signer_url: p("spiffe_agent.signer_url"), + signer_client_id: p("spiffe_agent.signer_client_id"), + signer_client_secret: p("spiffe_agent.signer_client_secret"), + signer_ca_cert_file: (p("spiffe_agent.signer_ca_cert", "").empty? ? "" : "#{conf_dir}/certs/signer/ca.crt"), + bbs_address: "https://#{p("spiffe_agent.bbs.api_location")}", + bbs_ca_cert_file: "#{conf_dir}/certs/bbs/ca.crt", + bbs_client_cert_file: "#{conf_dir}/certs/bbs/client.crt", + bbs_client_key_file: "#{conf_dir}/certs/bbs/client.key", + } + + config.to_json +%> diff --git a/packages/bbs/spec b/packages/bbs/spec index af84474341..91b8fbf33c 100644 --- a/packages/bbs/spec +++ b/packages/bbs/spec @@ -8,4 +8,4 @@ files: - code.cloudfoundry.org/go.mod - code.cloudfoundry.org/go.sum - code.cloudfoundry.org/vendor/modules.txt - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/*.go # gosub + - code.cloudfoundry.org/vendor/**/* diff --git a/packages/locket/spec b/packages/locket/spec index 4a0bdd933a..f63cfb5b7d 100644 --- a/packages/locket/spec +++ b/packages/locket/spec @@ -8,4 +8,4 @@ files: - code.cloudfoundry.org/go.mod - code.cloudfoundry.org/go.sum - code.cloudfoundry.org/vendor/modules.txt - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/*.go # gosub + - code.cloudfoundry.org/vendor/**/* diff --git a/packages/spiffe-agent/packaging b/packages/spiffe-agent/packaging new file mode 100644 index 0000000000..eca348ac60 --- /dev/null +++ b/packages/spiffe-agent/packaging @@ -0,0 +1,15 @@ +set -e + +# create GOPATH +mkdir -p ${BOSH_INSTALL_TARGET}/src +mv * ${BOSH_INSTALL_TARGET}/src +mv ${BOSH_INSTALL_TARGET}/src . + +source /var/vcap/packages/golang-*-linux/bosh/compile.env + +mkdir ${BOSH_INSTALL_TARGET}/bin +export GOBIN=${BOSH_INSTALL_TARGET}/bin + +pushd src/code.cloudfoundry.org +go install code.cloudfoundry.org/spiffe-agent/cmd/spiffe-agent +popd diff --git a/packages/spiffe-agent/spec b/packages/spiffe-agent/spec new file mode 100644 index 0000000000..a2f90deb99 --- /dev/null +++ b/packages/spiffe-agent/spec @@ -0,0 +1,153 @@ +--- +name: spiffe-agent + +dependencies: + - golang-1.26-linux + +files: + - code.cloudfoundry.org/go.mod + - code.cloudfoundry.org/go.sum + - code.cloudfoundry.org/vendor/modules.txt + - code.cloudfoundry.org/spiffe-agent/bbsresolver/*.go # gosub + - code.cloudfoundry.org/spiffe-agent/cfattestor/*.go # gosub + - code.cloudfoundry.org/spiffe-agent/cmd/spiffe-agent/*.go # gosub + - code.cloudfoundry.org/spiffe-agent/config/*.go # gosub + - code.cloudfoundry.org/spiffe-agent/signer/*.go # gosub + - code.cloudfoundry.org/spiffe-agent/workloadapi/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryption/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/format/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/models/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/trace/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/cfhttp/v2/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/internal/truncate/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/lagerflags/*.go # gosub + - code.cloudfoundry.org/vendor/code.cloudfoundry.org/tlsconfig/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/bmizerany/pat/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/gogo/protobuf/gogoproto/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/gogo/protobuf/proto/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/gogo/protobuf/protoc-gen-gogo/descriptor/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/gogo/protobuf/sortkeys/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/openzipkin/zipkin-go/idgenerator/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/openzipkin/zipkin-go/model/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/tedsuo/ifrit/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/tedsuo/ifrit/grouper/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/tedsuo/ifrit/sigmon/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/tedsuo/rata/*.go # gosub + - code.cloudfoundry.org/vendor/github.com/vito/go-sse/sse/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/http/httpguts/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/http2/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/http2/hpack/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/idna/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/internal/httpcommon/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/internal/httpsfv/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/internal/timeseries/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/net/trace/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/sys/unix/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/sys/unix/*.s # gosub + - code.cloudfoundry.org/vendor/golang.org/x/text/secure/bidirule/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/text/transform/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/text/unicode/bidi/*.go # gosub + - code.cloudfoundry.org/vendor/golang.org/x/text/unicode/norm/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/genproto/googleapis/rpc/status/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/attributes/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/backoff/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/base/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/endpointsharding/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/grpclb/state/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/pickfirst/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/pickfirst/internal/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/balancer/roundrobin/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/binarylog/grpc_binarylog_v1/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/channelz/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/codes/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/connectivity/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/credentials/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/credentials/insecure/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/encoding/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/encoding/internal/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/encoding/proto/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/experimental/stats/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/grpclog/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/grpclog/internal/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/backoff/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/balancer/gracefulswitch/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/balancer/weight/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/balancerload/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/binarylog/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/buffer/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/channelz/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/credentials/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/envconfig/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/grpclog/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/grpcsync/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/grpcutil/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/idle/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/mem/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/metadata/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/pretty/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/proxyattributes/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/delegatingresolver/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/dns/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/dns/internal/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/passthrough/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/resolver/unix/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/serviceconfig/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/stats/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/status/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/syscall/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/transport/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/transport/networktype/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/internal/transport/readyreader/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/keepalive/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/mem/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/metadata/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/peer/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/resolver/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/resolver/dns/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/serviceconfig/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/stats/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/status/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/grpc/tap/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/encoding/protojson/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/encoding/prototext/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/encoding/protowire/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/descfmt/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/descopts/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/detrand/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/editiondefaults/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/editiondefaults/editions_defaults.binpb + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/encoding/defval/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/encoding/json/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/encoding/messageset/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/encoding/tag/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/encoding/text/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/errors/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/filedesc/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/filetype/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/flags/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/genid/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/impl/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/order/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/pragma/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/protolazy/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/set/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/strs/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/internal/version/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/proto/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/protoadapt/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/reflect/protoreflect/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/reflect/protoregistry/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/runtime/protoiface/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/runtime/protoimpl/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/types/known/anypb/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/types/known/durationpb/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/types/known/structpb/*.go # gosub + - code.cloudfoundry.org/vendor/google.golang.org/protobuf/types/known/timestamppb/*.go # gosub diff --git a/src/code.cloudfoundry.org/executor b/src/code.cloudfoundry.org/executor index e99d5ce65d..244b804ce4 160000 --- a/src/code.cloudfoundry.org/executor +++ b/src/code.cloudfoundry.org/executor @@ -1 +1 @@ -Subproject commit e99d5ce65d4db660b6e66defaad6ef4db4ad4e69 +Subproject commit 244b804ce437834c0be06f048747621ab10b7794 diff --git a/src/code.cloudfoundry.org/go.mod b/src/code.cloudfoundry.org/go.mod index 9d8caa93b7..b6b8bfaed0 100644 --- a/src/code.cloudfoundry.org/go.mod +++ b/src/code.cloudfoundry.org/go.mod @@ -15,6 +15,7 @@ require ( code.cloudfoundry.org/bbs/encryption v1.7.0 code.cloudfoundry.org/bbs/format v1.7.0 code.cloudfoundry.org/bbs/models v1.6.0 + code.cloudfoundry.org/buildpackapplifecycle v0.0.0-20260504201830-3e265382f635 code.cloudfoundry.org/bytefmt v0.78.0 code.cloudfoundry.org/cacheddownloader v0.0.0-20250312193827-23c030d5e4f3 code.cloudfoundry.org/certsplitter v0.77.0 @@ -34,6 +35,7 @@ require ( code.cloudfoundry.org/localip v0.77.0 code.cloudfoundry.org/locket v1.3.0 code.cloudfoundry.org/routing-api v0.2.0 + code.cloudfoundry.org/routing-info v0.1.0 code.cloudfoundry.org/tlsconfig v0.60.0 github.com/GaryBoone/GoStats v0.0.0-20130122001700-1993eafbef57 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b @@ -70,6 +72,7 @@ require ( github.com/pborman/getopt v1.1.0 github.com/pkg/sftp v1.13.10 github.com/spf13/cobra v1.10.2 + github.com/spiffe/go-spiffe/v2 v2.8.1 github.com/square/certstrap v1.3.0 github.com/tedsuo/ifrit v0.0.0-20260418191334-846868129986 github.com/tedsuo/rata v1.0.0 @@ -81,7 +84,6 @@ require ( google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -172,4 +174,5 @@ require ( golang.org/x/tools v0.46.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/code.cloudfoundry.org/go.sum b/src/code.cloudfoundry.org/go.sum index 264ecbe79e..ca1fde04e1 100644 --- a/src/code.cloudfoundry.org/go.sum +++ b/src/code.cloudfoundry.org/go.sum @@ -604,6 +604,8 @@ code.cloudfoundry.org/bbs/format v1.7.0 h1:/QdRhrrNVu/nh/ioyiS+QVY1OAEq+ftYdUXrX code.cloudfoundry.org/bbs/format v1.7.0/go.mod h1:iheQ6y6V5iK+hfYKWbjJwLreBbZ9hoJWoaclcyWB3lA= code.cloudfoundry.org/bbs/models v1.6.0 h1:RF1md0/mAEQHuZwcy9KtYEnijRic47rfpbo5TcLMZm4= code.cloudfoundry.org/bbs/models v1.6.0/go.mod h1:MZg7u8Y+CkPwcO7LENtx6vdp3PIhP9g3H/fZdQgcx5Q= +code.cloudfoundry.org/buildpackapplifecycle v0.0.0-20260504201830-3e265382f635 h1:xHDIxbmaa2DkHc5xuNS5xWPUjHuTVxh7StNFN3vqis4= +code.cloudfoundry.org/buildpackapplifecycle v0.0.0-20260504201830-3e265382f635/go.mod h1:HYcRj8n4GH2YcnsrJP9BRPHIHUMTecbNIv1ILL8TaGI= code.cloudfoundry.org/bytefmt v0.78.0 h1:Aloep0LXiZ7OFviDbdmojNYZH5F10vWA6jpT5x6mwz4= code.cloudfoundry.org/bytefmt v0.78.0/go.mod h1:FtdIm5VQRERfop7h8xzQ33hhFZN+mEgz8S5v6xFWCLs= code.cloudfoundry.org/cacheddownloader v0.0.0-20250312193827-23c030d5e4f3 h1:SproHrcDI7MG25Smabm75/l4SkvvfUkEAvQOu7NSUKM= @@ -644,6 +646,8 @@ code.cloudfoundry.org/locket v1.3.0 h1:g0CJFfHC/fGjn6JtaluGTIxGw+0TRBzIe65ys2U7b code.cloudfoundry.org/locket v1.3.0/go.mod h1:xz8N8vJpm3qr1VT6GktvyWw5R6E0pVsDgI+tu26bXaw= code.cloudfoundry.org/routing-api v0.2.0 h1:eTlC499FLCwkvk73yFBAVp7+f8K/c0n7alS2N27v3ZY= code.cloudfoundry.org/routing-api v0.2.0/go.mod h1:Kp4wT5FacuYtU4sVvwSLcPiK0LL0+kElz5ixjhHAUeU= +code.cloudfoundry.org/routing-info v0.1.0 h1:BaH10yIoFIvK+ZD69eZ5wpjcEDySnAUyDiYEC/lXvfo= +code.cloudfoundry.org/routing-info v0.1.0/go.mod h1:/Fy+9LtYADaU//P5Pr0cUOYiRojHwTD3d8spkk531sI= code.cloudfoundry.org/tlsconfig v0.60.0 h1:bB+VlWZ1Y3RTHJBNMnUx0pnkSN0DrzmJI3A0g1YSnVw= code.cloudfoundry.org/tlsconfig v0.60.0/go.mod h1:twyxlqwOBbnU5uMc+GoS04cNuJvG7uXHC+gNAIkk81s= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -720,6 +724,7 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -1192,6 +1197,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= +github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/square/certstrap v1.3.0 h1:N9P0ZRA+DjT8pq5fGDj0z3FjafRKnBDypP0QHpMlaAk= github.com/square/certstrap v1.3.0/go.mod h1:wGZo9eE1B7WX2GKBn0htJ+B3OuRl2UsdCFySNooy9hU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/bbsresolver_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/bbsresolver_suite_test.go new file mode 100644 index 0000000000..fd4a0ae42f --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/bbsresolver_suite_test.go @@ -0,0 +1,13 @@ +package bbsresolver_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestBbsresolver(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Bbsresolver Suite") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver.go b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver.go new file mode 100644 index 0000000000..d2e57f6120 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver.go @@ -0,0 +1,55 @@ +package bbsresolver + +import ( + "context" + "fmt" + + "code.cloudfoundry.org/bbs" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +// Resolver maps a Garden container handle to its CF process type by querying BBS. +type Resolver struct { + bbs bbs.InternalClient + cellID string + logger lager.Logger +} + +// New returns a Resolver that queries client for ActualLRPs on cellID. +func New(client bbs.InternalClient, cellID string, logger lager.Logger) *Resolver { + return &Resolver{bbs: client, cellID: cellID, logger: logger} +} + +// ProcessType resolves a Garden handle (== ActualLRP.InstanceGuid) to its +// process type. It locates the matching ActualLRP on the cell, fetches the +// DesiredLRP for its ProcessGuid, and reads the process_type metric tag. +func (r *Resolver) ProcessType(ctx context.Context, handle string) (string, error) { + lrps, err := r.bbs.ActualLRPs(r.logger, "", models.ActualLRPFilter{CellID: r.cellID}) + if err != nil { + return "", err + } + + var match *models.ActualLRP + for _, lrp := range lrps { + if lrp.InstanceGuid == handle { + match = lrp + break + } + } + if match == nil { + return "", fmt.Errorf("no actual lrp for handle %q on cell %q", handle, r.cellID) + } + + desired, err := r.bbs.DesiredLRPByProcessGuid(r.logger, "", match.ProcessGuid) + if err != nil { + return "", err + } + + tag := desired.GetMetricTags()["process_type"] + if tag == nil || tag.Static == "" { + return "", fmt.Errorf("desired lrp %q has no process_type metric tag", match.ProcessGuid) + } + + return tag.Static, nil +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver_test.go b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver_test.go new file mode 100644 index 0000000000..531c8e3240 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/bbsresolver/resolver_test.go @@ -0,0 +1,99 @@ +package bbsresolver_test + +import ( + "context" + "errors" + + "code.cloudfoundry.org/bbs/fake_bbs" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/spiffe-agent/bbsresolver" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Resolver", func() { + var ( + fakeBBS *fake_bbs.FakeInternalClient + resolver *bbsresolver.Resolver + ctx context.Context + ) + + const cellID = "cell-1" + + BeforeEach(func() { + fakeBBS = new(fake_bbs.FakeInternalClient) + logger := lagertest.NewTestLogger("bbsresolver") + resolver = bbsresolver.New(fakeBBS, cellID, logger) + ctx = context.Background() + }) + + Describe("ProcessType", func() { + It("resolves the handle to its process type", func() { + fakeBBS.ActualLRPsReturns([]*models.ActualLRP{{ + ActualLRPKey: models.ActualLRPKey{ProcessGuid: "pg"}, + ActualLRPInstanceKey: models.ActualLRPInstanceKey{InstanceGuid: "handle-1"}, + }}, nil) + fakeBBS.DesiredLRPByProcessGuidReturns(&models.DesiredLRP{ + MetricTags: map[string]*models.MetricTagValue{"process_type": {Static: "web"}}, + }, nil) + + processType, err := resolver.ProcessType(ctx, "handle-1") + + Expect(err).NotTo(HaveOccurred()) + Expect(processType).To(Equal("web")) + }) + + It("filters ActualLRPs to the resolver's cell", func() { + fakeBBS.ActualLRPsReturns([]*models.ActualLRP{{ + ActualLRPKey: models.ActualLRPKey{ProcessGuid: "pg"}, + ActualLRPInstanceKey: models.ActualLRPInstanceKey{InstanceGuid: "handle-1"}, + }}, nil) + fakeBBS.DesiredLRPByProcessGuidReturns(&models.DesiredLRP{ + MetricTags: map[string]*models.MetricTagValue{"process_type": {Static: "web"}}, + }, nil) + + _, err := resolver.ProcessType(ctx, "handle-1") + + Expect(err).NotTo(HaveOccurred()) + Expect(fakeBBS.ActualLRPsCallCount()).To(Equal(1)) + _, _, filter := fakeBBS.ActualLRPsArgsForCall(0) + Expect(filter.CellID).To(Equal(cellID)) + _, _, processGuid := fakeBBS.DesiredLRPByProcessGuidArgsForCall(0) + Expect(processGuid).To(Equal("pg")) + }) + + It("returns an error when no LRP on the cell matches the handle", func() { + fakeBBS.ActualLRPsReturns([]*models.ActualLRP{{ + ActualLRPKey: models.ActualLRPKey{ProcessGuid: "pg"}, + ActualLRPInstanceKey: models.ActualLRPInstanceKey{InstanceGuid: "other-handle"}, + }}, nil) + + _, err := resolver.ProcessType(ctx, "handle-1") + + Expect(err).To(HaveOccurred()) + Expect(fakeBBS.DesiredLRPByProcessGuidCallCount()).To(Equal(0)) + }) + + It("propagates BBS errors from ActualLRPs", func() { + fakeBBS.ActualLRPsReturns(nil, errors.New("boom")) + + _, err := resolver.ProcessType(ctx, "handle-1") + + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when the process_type metric tag is missing", func() { + fakeBBS.ActualLRPsReturns([]*models.ActualLRP{{ + ActualLRPKey: models.ActualLRPKey{ProcessGuid: "pg"}, + ActualLRPInstanceKey: models.ActualLRPInstanceKey{InstanceGuid: "handle-1"}, + }}, nil) + fakeBBS.DesiredLRPByProcessGuidReturns(&models.DesiredLRP{}, nil) + + _, err := resolver.ProcessType(ctx, "handle-1") + + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor.go new file mode 100644 index 0000000000..26b023df6b --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor.go @@ -0,0 +1,115 @@ +package cfattestor + +import ( + "bufio" + "context" + "crypto" + "crypto/x509" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// Attestation is the full result of attesting a workload pid. +type Attestation struct { + Selectors Selectors + CertPEM string + InstanceCert *x509.Certificate + InstanceKey crypto.Signer +} + +// ProcessTypeResolver resolves the CF process type for a container handle. +type ProcessTypeResolver interface { + ProcessType(ctx context.Context, handle string) (string, error) +} + +// Attestor turns a pid into an Attestation using the container's cgroup, +// instance credentials, and process tree. +type Attestor struct { + procRoot string + resolver ProcessTypeResolver +} + +// New returns an Attestor that reads from /proc. +func New(resolver ProcessTypeResolver) *Attestor { + return &Attestor{procRoot: "/proc", resolver: resolver} +} + +// Attest resolves the workload identity for pid. SSH sessions are rejected. +func (a *Attestor) Attest(ctx context.Context, pid int) (Attestation, error) { + handle, err := containerHandle(a.procRoot, pid) + if err != nil { + return Attestation{}, err + } + + ssh, err := hasSSHAncestor(a.procRoot, pid) + if err != nil { + return Attestation{}, err + } + if ssh { + return Attestation{}, fmt.Errorf("ssh sessions are not attestable") + } + + certPEM, cert, key, err := readInstanceCredentials(a.procRoot, pid) + if err != nil { + return Attestation{}, err + } + + selectors, err := parseSelectors(cert) + if err != nil { + return Attestation{}, err + } + + procType, err := a.resolver.ProcessType(ctx, handle) + if err != nil { + return Attestation{}, err + } + selectors.ProcessType = procType + + return Attestation{Selectors: selectors, CertPEM: certPEM, InstanceCert: cert, InstanceKey: key}, nil +} + +// hasSSHAncestor walks the process tree from pid upward (max 20 hops, stopping +// at pid 1) and returns true if any ancestor is diego-sshd. +func hasSSHAncestor(procRoot string, pid int) (bool, error) { + for hops := 0; hops < 20 && pid > 1; hops++ { + name, ppid, err := readStatus(procRoot, pid) + if err != nil { + return false, err + } + if name == "diego-sshd" { + return true, nil + } + pid = ppid + } + return false, nil +} + +// readStatus parses Name: and PPid: from //status. +func readStatus(procRoot string, pid int) (name string, ppid int, err error) { + f, err := os.Open(filepath.Join(procRoot, strconv.Itoa(pid), "status")) + if err != nil { + return "", 0, err + } + defer f.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + line := sc.Text() + switch { + case strings.HasPrefix(line, "Name:"): + name = strings.TrimSpace(strings.TrimPrefix(line, "Name:")) + case strings.HasPrefix(line, "PPid:"): + ppid, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "PPid:"))) + } + } + return name, ppid, sc.Err() +} + +// BuildSpiffeID builds the canonical CF SPIFFE ID for s. +func BuildSpiffeID(trustDomain string, s Selectors) string { + return fmt.Sprintf("spiffe://%s/cf/org/%s/space/%s/app/%s/process/%s", + trustDomain, s.OrgID, s.SpaceID, s.AppID, s.ProcessType) +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor_test.go new file mode 100644 index 0000000000..b101e62762 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/attestor_test.go @@ -0,0 +1,63 @@ +package cfattestor + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type stubResolver struct { + typ string + err error +} + +func (r stubResolver) ProcessType(ctx context.Context, handle string) (string, error) { + return r.typ, r.err +} + +var _ = Describe("Attestor.Attest", func() { + const pid = 1000 + var procRoot string + + BeforeEach(func() { + procRoot = GinkgoT().TempDir() + }) + + It("assembles a full Attestation with the resolved process type", func() { + setupContainer(procRoot, pid, "handle-abc", Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3"}) + + a := New(stubResolver{typ: "web"}) + a.procRoot = procRoot + + att, err := a.Attest(context.Background(), pid) + Expect(err).NotTo(HaveOccurred()) + Expect(att.Selectors).To(Equal(Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3", ProcessType: "web"})) + Expect(att.CertPEM).NotTo(BeEmpty()) + Expect(att.InstanceCert).NotTo(BeNil()) + Expect(att.InstanceKey).NotTo(BeNil()) + }) + + It("rejects ssh sessions", func() { + setupContainer(procRoot, pid, "handle-abc", Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3"}) + writeStatus(procRoot, pid, "web", 999) + writeStatus(procRoot, 999, "diego-sshd", 1) + + a := New(stubResolver{typ: "web"}) + a.procRoot = procRoot + + _, err := a.Attest(context.Background(), pid) + Expect(err).To(MatchError(ContainSubstring("ssh sessions are not attestable"))) + }) + + It("propagates resolver errors", func() { + setupContainer(procRoot, pid, "handle-abc", Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3"}) + + a := New(stubResolver{err: fmt.Errorf("boom")}) + a.procRoot = procRoot + + _, err := a.Attest(context.Background(), pid) + Expect(err).To(MatchError(ContainSubstring("boom"))) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/cfattestor_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/cfattestor_suite_test.go new file mode 100644 index 0000000000..d537ecafdf --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/cfattestor_suite_test.go @@ -0,0 +1,13 @@ +package cfattestor + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCfattestor(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cfattestor Suite") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle.go new file mode 100644 index 0000000000..1c3a258703 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle.go @@ -0,0 +1,40 @@ +package cfattestor + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// containerHandle reads //cgroup and returns the Garden handle. +func containerHandle(procRoot string, pid int) (string, error) { + content, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(pid), "cgroup")) + if err != nil { + return "", err + } + + for _, line := range strings.Split(string(content), "\n") { + fields := strings.Split(line, ":") + path := fields[len(fields)-1] + if handle, ok := handleFromPath(path); ok { + return handle, nil + } + } + + return "", fmt.Errorf("no garden cgroup for pid %d", pid) +} + +// handleFromPath extracts the path segment immediately following "garden". +// v1 line: "::/garden/" +// v2 line: "0::/garden//init" (or deeper) +func handleFromPath(p string) (string, bool) { + segments := strings.Split(p, "/") + for i, seg := range segments { + if seg == "garden" && i+1 < len(segments) && segments[i+1] != "" { + return segments[i+1], true + } + } + return "", false +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle_test.go new file mode 100644 index 0000000000..eb409d71ab --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/handle_test.go @@ -0,0 +1,62 @@ +package cfattestor + +import ( + "os" + "path/filepath" + "strconv" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("containerHandle", func() { + const pid = 4242 + var procRoot string + + writeCgroup := func(content string) { + dir := filepath.Join(procRoot, strconv.Itoa(pid)) + Expect(os.MkdirAll(dir, 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(dir, "cgroup"), []byte(content), 0644)).To(Succeed()) + } + + BeforeEach(func() { + procRoot = GinkgoT().TempDir() + }) + + DescribeTable("resolves the Garden handle", + func(content, expected string) { + writeCgroup(content) + handle, err := containerHandle(procRoot, pid) + Expect(err).NotTo(HaveOccurred()) + Expect(handle).To(Equal(expected)) + }, + Entry("cgroup v1 controller line", "12:cpu,cpuacct:/garden/abc123\n", "abc123"), + Entry("cgroup v2 unified line", "0::/garden/abc123/init\n", "abc123"), + Entry("mixed multi-line", "11:pids:/system.slice\n12:cpu,cpuacct:/garden/abc123\n0::/garden/abc123/init\n", "abc123"), + ) + + It("errors when no garden segment exists", func() { + writeCgroup("0::/system.slice/foo.service\n") + _, err := containerHandle(procRoot, pid) + Expect(err).To(HaveOccurred()) + }) + + It("errors when the cgroup file is unreadable", func() { + _, err := containerHandle(procRoot, pid) + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("handleFromPath", func() { + DescribeTable("extracts the segment after garden", + func(path, expected string, ok bool) { + handle, found := handleFromPath(path) + Expect(found).To(Equal(ok)) + Expect(handle).To(Equal(expected)) + }, + Entry("v1 path", "/garden/abc123", "abc123", true), + Entry("v2 init path", "/garden/abc123/init", "abc123", true), + Entry("no garden", "/system.slice/foo.service", "", false), + Entry("garden trailing with nothing", "/garden", "", false), + ) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/helpers_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/helpers_test.go new file mode 100644 index 0000000000..58484902b3 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/helpers_test.go @@ -0,0 +1,39 @@ +package cfattestor + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + + . "github.com/onsi/gomega" +) + +// writeCgroup writes a fake //cgroup file. +func writeCgroup(procRoot string, pid int, content string) { + dir := filepath.Join(procRoot, strconv.Itoa(pid)) + ExpectWithOffset(1, os.MkdirAll(dir, 0755)).To(Succeed()) + ExpectWithOffset(1, os.WriteFile(filepath.Join(dir, "cgroup"), []byte(content), 0644)).To(Succeed()) +} + +// writeStatus writes a fake //status with Name: and PPid: lines. +func writeStatus(procRoot string, pid int, name string, ppid int) { + dir := filepath.Join(procRoot, strconv.Itoa(pid)) + ExpectWithOffset(1, os.MkdirAll(dir, 0755)).To(Succeed()) + content := fmt.Sprintf("Name:\t%s\nPPid:\t%d\n", name, ppid) + ExpectWithOffset(1, os.WriteFile(filepath.Join(dir, "status"), []byte(content), 0644)).To(Succeed()) +} + +// setupContainer wires up a fake pid: cgroup with the handle, a non-ssh status +// (Name=web, PPid=1), and instance credentials carrying s. Certs are minted +// inline via mintInstanceCert because a white-box test cannot import testsupport. +func setupContainer(procRoot string, pid int, handle string, s Selectors) { + writeCgroup(procRoot, pid, "0::/garden/"+handle+"/init\n") + writeStatus(procRoot, pid, "web", 1) + + certPEM, keyPEM, _ := mintInstanceCert(handle, s) + credDir := filepath.Join(procRoot, strconv.Itoa(pid), "root", "etc", "cf-instance-credentials") + ExpectWithOffset(1, os.MkdirAll(credDir, 0755)).To(Succeed()) + ExpectWithOffset(1, os.WriteFile(filepath.Join(credDir, "instance.crt"), []byte(certPEM), 0644)).To(Succeed()) + ExpectWithOffset(1, os.WriteFile(filepath.Join(credDir, "instance.key"), []byte(keyPEM), 0600)).To(Succeed()) +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity.go new file mode 100644 index 0000000000..4c0b22f0ce --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity.go @@ -0,0 +1,99 @@ +package cfattestor + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// Selectors are the CF workload identity attributes parsed from a container's +// instance certificate. ProcessType is filled in by the resolver, not here. +type Selectors struct { + OrgID string + SpaceID string + AppID string + ProcessType string +} + +// readInstanceCredentials reads the CF instance certificate and key from the +// container's view at //root/etc/cf-instance-credentials and +// returns the raw cert PEM, the parsed certificate, and the private key signer. +func readInstanceCredentials(procRoot string, pid int) (certPEM string, cert *x509.Certificate, key crypto.Signer, err error) { + base := filepath.Join(procRoot, strconv.Itoa(pid), "root", "etc", "cf-instance-credentials") + + crtBytes, err := os.ReadFile(filepath.Join(base, "instance.crt")) + if err != nil { + return "", nil, nil, err + } + block, _ := pem.Decode(crtBytes) + if block == nil { + return "", nil, nil, fmt.Errorf("instance.crt: no PEM data") + } + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return "", nil, nil, fmt.Errorf("instance.crt: %w", err) + } + + keyBytes, err := os.ReadFile(filepath.Join(base, "instance.key")) + if err != nil { + return "", nil, nil, err + } + key, err = parsePrivateKey(keyBytes) + if err != nil { + return "", nil, nil, fmt.Errorf("instance.key: %w", err) + } + + return string(crtBytes), cert, key, nil +} + +// parseSelectors extracts CF selectors from the certificate's OU entries, which +// are encoded as "organization:", "space:", "app:". OU order is not +// significant (x509 stores them as a SET). ProcessType is left empty. +func parseSelectors(cert *x509.Certificate) (Selectors, error) { + var s Selectors + for _, ou := range cert.Subject.OrganizationalUnit { + key, val, ok := strings.Cut(ou, ":") + if !ok { + continue + } + switch key { + case "organization": + s.OrgID = val + case "space": + s.SpaceID = val + case "app": + s.AppID = val + } + } + if s.OrgID == "" && s.SpaceID == "" && s.AppID == "" { + return Selectors{}, fmt.Errorf("no CF selectors in certificate OUs") + } + return s, nil +} + +// parsePrivateKey decodes a PEM private key, trying PKCS8, then PKCS1, then EC. +func parsePrivateKey(pemBytes []byte) (crypto.Signer, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("no PEM data") + } + if k, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { + signer, ok := k.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("PKCS8 key is not a signer") + } + return signer, nil + } + if k, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return k, nil + } + if k, err := x509.ParseECPrivateKey(block.Bytes); err == nil { + return k, nil + } + return nil, fmt.Errorf("unsupported or malformed private key") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity_test.go new file mode 100644 index 0000000000..2dfe8315aa --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/identity_test.go @@ -0,0 +1,99 @@ +package cfattestor + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// mintInstanceCert mirrors testsupport.MintInstanceCert; inlined here because a +// white-box test cannot import testsupport (which imports this package). +func mintInstanceCert(handle string, s Selectors) (certPEM, keyPEM string, key crypto.Signer) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: handle, + OrganizationalUnit: []string{"organization:" + s.OrgID, "space:" + s.SpaceID, "app:" + s.AppID}, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + Expect(err).NotTo(HaveOccurred()) + pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) + Expect(err).NotTo(HaveOccurred()) + certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) + keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})) + return certPEM, keyPEM, priv +} + +var _ = Describe("readInstanceCredentials", func() { + const pid = 7777 + var procRoot, credDir string + + BeforeEach(func() { + procRoot = GinkgoT().TempDir() + credDir = filepath.Join(procRoot, strconv.Itoa(pid), "root", "etc", "cf-instance-credentials") + Expect(os.MkdirAll(credDir, 0755)).To(Succeed()) + }) + + writeCreds := func(certPEM, keyPEM string) { + Expect(os.WriteFile(filepath.Join(credDir, "instance.crt"), []byte(certPEM), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(credDir, "instance.key"), []byte(keyPEM), 0600)).To(Succeed()) + } + + It("returns the cert PEM, parsed cert, signer, and decodable selectors", func() { + certPEM, keyPEM, _ := mintInstanceCert("handle-abc", Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3"}) + writeCreds(certPEM, keyPEM) + + gotPEM, cert, key, err := readInstanceCredentials(procRoot, pid) + Expect(err).NotTo(HaveOccurred()) + Expect(gotPEM).To(Equal(certPEM)) + Expect(cert.Subject.CommonName).To(Equal("handle-abc")) + Expect(key).NotTo(BeNil()) + + parsed, err := parseSelectors(cert) + Expect(err).NotTo(HaveOccurred()) + Expect(parsed).To(Equal(Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3"})) + Expect(parsed.ProcessType).To(BeEmpty()) + }) + + It("errors when the cert file is missing", func() { + _, _, _, err := readInstanceCredentials(procRoot, pid) + Expect(err).To(HaveOccurred()) + }) + + It("errors when the key is malformed", func() { + certPEM, _, _ := mintInstanceCert("h", Selectors{OrgID: "o", SpaceID: "s", AppID: "a"}) + writeCreds(certPEM, "-----BEGIN PRIVATE KEY-----\nbogus\n-----END PRIVATE KEY-----\n") + _, _, _, err := readInstanceCredentials(procRoot, pid) + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("parsePrivateKey", func() { + It("parses the PKCS8 key", func() { + _, keyPEM, _ := mintInstanceCert("h", Selectors{}) + signer, err := parsePrivateKey([]byte(keyPEM)) + Expect(err).NotTo(HaveOccurred()) + Expect(signer).NotTo(BeNil()) + }) + + It("errors on garbage input", func() { + _, err := parsePrivateKey([]byte("not a key")) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/cfattestor/spiffeid_test.go b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/spiffeid_test.go new file mode 100644 index 0000000000..faf67c5add --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cfattestor/spiffeid_test.go @@ -0,0 +1,14 @@ +package cfattestor + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildSpiffeID", func() { + It("builds the golden CF SPIFFE ID", func() { + s := Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3", ProcessType: "web"} + Expect(BuildSpiffeID("example.org", s)).To(Equal( + "spiffe://example.org/cf/org/org-1/space/space-2/app/app-3/process/web")) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/cmd/spiffe-agent/main.go b/src/code.cloudfoundry.org/spiffe-agent/cmd/spiffe-agent/main.go new file mode 100644 index 0000000000..d9a4630fbe --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/cmd/spiffe-agent/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "os" + "time" + + "code.cloudfoundry.org/bbs" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagerflags" + "code.cloudfoundry.org/spiffe-agent/bbsresolver" + "code.cloudfoundry.org/spiffe-agent/cfattestor" + "code.cloudfoundry.org/spiffe-agent/config" + "code.cloudfoundry.org/spiffe-agent/signer" + "code.cloudfoundry.org/spiffe-agent/workloadapi" + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/grouper" + "github.com/tedsuo/ifrit/sigmon" +) + +// defaultBundleRefreshInterval is how often FetchJWTBundles re-fetches UAA's +// token_keys to detect signing-key rotation. +const defaultBundleRefreshInterval = 60 * time.Second + +var configFilePath = flag.String( + "config", + "", + "Path to JSON configuration file", +) + +func main() { + flag.Parse() + + cfg, err := config.NewSpiffeAgentConfig(*configFilePath) + if err != nil { + panic(err) + } + + logger, _ := lagerflags.NewFromConfig("spiffe-agent", cfg.LagerConfig) + + bbsClient := initializeBBSClient(logger, cfg) + resolver := bbsresolver.New(bbsClient, cfg.CellID, logger) + attestor := cfattestor.New(resolver) + + httpClient, err := cfg.SignerHTTPClient() + if err != nil { + logger.Fatal("failed-to-build-signer-http-client", err) + } + sgnr := signer.New(httpClient, cfg.SignerURL, cfg.SignerClientID, cfg.SignerClientSecret, cfg.TrustDomain) + bundleSource := signer.NewBundleSource(httpClient, cfg.SignerURL+"/token_keys", cfg.TrustDomain) + + srv := workloadapi.NewServer(attestor, sgnr, bundleSource, defaultBundleRefreshInterval) + + members := grouper.Members{ + {Name: "workload-api", Runner: workloadapi.NewRunner(cfg.SocketPath, srv, logger)}, + } + + group := grouper.NewOrdered(os.Interrupt, members) + monitor := ifrit.Invoke(sigmon.New(group)) + + logger.Info("started") + + err = <-monitor.Wait() + if err != nil { + logger.Error("exited-with-failure", err) + os.Exit(1) + } + + logger.Info("exited") +} + +func initializeBBSClient(logger lager.Logger, cfg config.Config) bbs.InternalClient { + bbsClient, err := bbs.NewClientWithConfig(bbs.ClientConfig{ + URL: cfg.BBSAddress, + IsTLS: true, + CAFile: cfg.BBSCACertFile, + CertFile: cfg.BBSClientCertFile, + KeyFile: cfg.BBSClientKeyFile, + }) + if err != nil { + logger.Fatal("failed-to-configure-secure-bbs-client", err) + } + return bbsClient +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/config/config.go b/src/code.cloudfoundry.org/spiffe-agent/config/config.go new file mode 100644 index 0000000000..3991108085 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/config/config.go @@ -0,0 +1,75 @@ +package config + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "code.cloudfoundry.org/lager/v3/lagerflags" +) + +const signerHTTPTimeout = 30 * time.Second + +type Config struct { + lagerflags.LagerConfig + SocketPath string `json:"socket_path"` + TrustDomain string `json:"trust_domain"` + CellID string `json:"cell_id"` + SignerURL string `json:"signer_url"` + SignerClientID string `json:"signer_client_id"` + SignerClientSecret string `json:"signer_client_secret"` + SignerCACertFile string `json:"signer_ca_cert_file"` + BBSAddress string `json:"bbs_address"` + BBSCACertFile string `json:"bbs_ca_cert_file"` + BBSClientCertFile string `json:"bbs_client_cert_file"` + BBSClientKeyFile string `json:"bbs_client_key_file"` +} + +func defaultConfig() Config { + return Config{ + LagerConfig: lagerflags.DefaultLagerConfig(), + SocketPath: "/var/vcap/data/spiffe-agent/run/workload.sock", + } +} + +func NewSpiffeAgentConfig(path string) (Config, error) { + cfg := defaultConfig() + + contents, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } + + if err := json.Unmarshal(contents, &cfg); err != nil { + return Config{}, err + } + + return cfg, nil +} + +func (c Config) SignerHTTPClient() (*http.Client, error) { + if c.SignerCACertFile == "" { + return &http.Client{Timeout: signerHTTPTimeout}, nil + } + + caCert, err := os.ReadFile(c.SignerCACertFile) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("no valid certs in %s", c.SignerCACertFile) + } + + return &http.Client{ + Timeout: signerHTTPTimeout, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: pool}, + }, + }, nil +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/config/config_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/config/config_suite_test.go new file mode 100644 index 0000000000..c6e29ba716 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Suite") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/config/config_test.go b/src/code.cloudfoundry.org/spiffe-agent/config/config_test.go new file mode 100644 index 0000000000..2e89243f98 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/config/config_test.go @@ -0,0 +1,155 @@ +package config_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/http" + "os" + "path/filepath" + "time" + + "code.cloudfoundry.org/lager/v3/lagerflags" + "code.cloudfoundry.org/spiffe-agent/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// writeCAPEM generates a self-signed CA certificate and writes it as PEM to a +// temp file, returning the file path so RootCAs population can be exercised. +func writeCAPEM(dir string) string { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + Expect(err).NotTo(HaveOccurred()) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "spiffe-agent-test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + + caPath := filepath.Join(dir, "ca.pem") + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + Expect(os.WriteFile(caPath, pemBytes, 0644)).To(Succeed()) + return caPath +} + +var _ = Describe("Config", func() { + var dir string + + BeforeEach(func() { + dir = GinkgoT().TempDir() + }) + + Describe("NewSpiffeAgentConfig", func() { + It("round-trips every field from JSON", func() { + json := `{ + "socket_path": "/run/workload.sock", + "trust_domain": "example.org", + "cell_id": "cell-1", + "signer_url": "https://uaa.example.org", + "signer_client_id": "spiffe-signer", + "signer_client_secret": "s3cr3t", + "signer_ca_cert_file": "/etc/uaa/ca.pem", + "bbs_address": "https://bbs.example.org", + "bbs_ca_cert_file": "/etc/bbs/ca.pem", + "bbs_client_cert_file": "/etc/bbs/cert.pem", + "bbs_client_key_file": "/etc/bbs/key.pem", + "log_level": "debug" + }` + path := filepath.Join(dir, "config.json") + Expect(os.WriteFile(path, []byte(json), 0644)).To(Succeed()) + + cfg, err := config.NewSpiffeAgentConfig(path) + Expect(err).NotTo(HaveOccurred()) + + Expect(cfg.SocketPath).To(Equal("/run/workload.sock")) + Expect(cfg.TrustDomain).To(Equal("example.org")) + Expect(cfg.CellID).To(Equal("cell-1")) + Expect(cfg.SignerURL).To(Equal("https://uaa.example.org")) + Expect(cfg.SignerClientID).To(Equal("spiffe-signer")) + Expect(cfg.SignerClientSecret).To(Equal("s3cr3t")) + Expect(cfg.SignerCACertFile).To(Equal("/etc/uaa/ca.pem")) + Expect(cfg.BBSAddress).To(Equal("https://bbs.example.org")) + Expect(cfg.BBSCACertFile).To(Equal("/etc/bbs/ca.pem")) + Expect(cfg.BBSClientCertFile).To(Equal("/etc/bbs/cert.pem")) + Expect(cfg.BBSClientKeyFile).To(Equal("/etc/bbs/key.pem")) + Expect(cfg.LogLevel).To(Equal("debug")) + }) + + It("defaults SocketPath when omitted", func() { + path := filepath.Join(dir, "config.json") + Expect(os.WriteFile(path, []byte(`{"trust_domain":"example.org"}`), 0644)).To(Succeed()) + + cfg, err := config.NewSpiffeAgentConfig(path) + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.SocketPath).To(Equal("/var/vcap/data/spiffe-agent/run/workload.sock")) + }) + + It("defaults LagerConfig when omitted", func() { + path := filepath.Join(dir, "config.json") + Expect(os.WriteFile(path, []byte(`{"trust_domain":"example.org"}`), 0644)).To(Succeed()) + + cfg, err := config.NewSpiffeAgentConfig(path) + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.LagerConfig).To(Equal(lagerflags.DefaultLagerConfig())) + }) + + It("errors on a missing file", func() { + _, err := config.NewSpiffeAgentConfig(filepath.Join(dir, "does-not-exist.json")) + Expect(err).To(HaveOccurred()) + }) + + It("errors on malformed JSON", func() { + path := filepath.Join(dir, "bad.json") + Expect(os.WriteFile(path, []byte("{not json"), 0644)).To(Succeed()) + + _, err := config.NewSpiffeAgentConfig(path) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("SignerHTTPClient", func() { + It("returns a non-nil client with a sane timeout when no CA is configured", func() { + cfg := config.Config{} + client, err := cfg.SignerHTTPClient() + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + Expect(client.Timeout).To(Equal(30 * time.Second)) + }) + + It("populates RootCAs from the configured CA file", func() { + cfg := config.Config{SignerCACertFile: writeCAPEM(dir)} + client, err := cfg.SignerHTTPClient() + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + Expect(client.Timeout).To(Equal(30 * time.Second)) + + transport, ok := client.Transport.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(transport.TLSClientConfig).NotTo(BeNil()) + Expect(transport.TLSClientConfig.RootCAs).NotTo(BeNil()) + Expect(transport.TLSClientConfig.RootCAs.Subjects()).To(HaveLen(1)) + }) + + It("errors when the CA file contains no valid certs", func() { + path := filepath.Join(dir, "garbage.pem") + Expect(os.WriteFile(path, []byte("not a pem"), 0644)).To(Succeed()) + + cfg := config.Config{SignerCACertFile: path} + _, err := cfg.SignerHTTPClient() + Expect(err).To(MatchError(ContainSubstring("no valid certs"))) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint.go b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint.go new file mode 100644 index 0000000000..d7eafe126b --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint.go @@ -0,0 +1,55 @@ +// Package testsupport provides certificate-minting helpers shared across +// spiffe-agent tests. It is imported only by _test.go files; production +// packages such as cfattestor must never depend on it. +package testsupport + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + "code.cloudfoundry.org/spiffe-agent/cfattestor" +) + +// MintInstanceCert builds a self-signed RSA-2048 certificate whose CN is the +// container handle and whose OU entries encode the CF selectors in the order +// organization, space, app. It returns the PEM-encoded cert, the PKCS8 +// PEM-encoded key, and the signer. Errors are fatal helper bugs and panic. +func MintInstanceCert(handle string, s cfattestor.Selectors) (certPEM, keyPEM string, key crypto.Signer) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: handle, + OrganizationalUnit: []string{ + "organization:" + s.OrgID, + "space:" + s.SpaceID, + "app:" + s.AppID, + }, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + if err != nil { + panic(err) + } + pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + panic(err) + } + + certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) + keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})) + return certPEM, keyPEM, priv +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint_test.go b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint_test.go new file mode 100644 index 0000000000..0589a2c4de --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/mint_test.go @@ -0,0 +1,31 @@ +package testsupport_test + +import ( + "crypto/x509" + "encoding/pem" + + "code.cloudfoundry.org/spiffe-agent/cfattestor" + "code.cloudfoundry.org/spiffe-agent/internal/testsupport" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("MintInstanceCert", func() { + It("mints a self-signed RSA cert with CN=handle and selector OUs", func() { + certPEM, keyPEM, key := testsupport.MintInstanceCert("habc", cfattestor.Selectors{OrgID: "o1", SpaceID: "s2", AppID: "a3"}) + Expect(key).NotTo(BeNil()) + + block, _ := pem.Decode([]byte(certPEM)) + Expect(block).NotTo(BeNil()) + cert, err := x509.ParseCertificate(block.Bytes) + Expect(err).NotTo(HaveOccurred()) + Expect(cert.Subject.CommonName).To(Equal("habc")) + Expect(cert.Subject.OrganizationalUnit).To(ConsistOf("organization:o1", "space:s2", "app:a3")) + + kb, _ := pem.Decode([]byte(keyPEM)) + Expect(kb).NotTo(BeNil()) + _, err = x509.ParsePKCS8PrivateKey(kb.Bytes) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/testsupport_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/testsupport_suite_test.go new file mode 100644 index 0000000000..a15f768fbe --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/internal/testsupport/testsupport_suite_test.go @@ -0,0 +1,13 @@ +package testsupport_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTestsupport(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testsupport Suite") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/signer/bundle.go b/src/code.cloudfoundry.org/spiffe-agent/signer/bundle.go new file mode 100644 index 0000000000..b8fe5289c5 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/signer/bundle.go @@ -0,0 +1,61 @@ +package signer + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// BundleSource fetches the JWT trust bundle (a JWKS document) from UAA's +// token_keys endpoint and publishes it keyed by the trust domain's SPIFFE ID, +// as required by the SPIFFE Workload API FetchJWTBundles RPC. The JWKS bytes are +// passed through verbatim so go-spiffe clients parse exactly what UAA publishes. +type BundleSource struct { + httpClient *http.Client + url string + trustDomain string +} + +// NewBundleSource returns a BundleSource that GETs url (UAA's token_keys +// endpoint) and publishes the JWKS under spiffe://. +func NewBundleSource(c *http.Client, url, trustDomain string) *BundleSource { + return &BundleSource{httpClient: c, url: url, trustDomain: trustDomain} +} + +// Bundles fetches the current JWKS and returns it keyed by the trust domain +// SPIFFE ID. It fails fast when the response is not a non-empty JWKS so a +// misconfigured endpoint surfaces as an error rather than an unparsable bundle. +func (b *BundleSource) Bundles(ctx context.Context) (map[string][]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, b.url, nil) + if err != nil { + return nil, err + } + + resp, err := b.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("token_keys: status %d: %s", resp.StatusCode, body) + } + + var jwks struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.Unmarshal(body, &jwks); err != nil { + return nil, fmt.Errorf("token_keys: invalid JWKS: %w", err) + } + if len(jwks.Keys) == 0 { + return nil, fmt.Errorf("token_keys: JWKS contains no keys") + } + + return map[string][]byte{"spiffe://" + b.trustDomain: body}, nil +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/signer/bundle_test.go b/src/code.cloudfoundry.org/spiffe-agent/signer/bundle_test.go new file mode 100644 index 0000000000..35a2e5ecb3 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/signer/bundle_test.go @@ -0,0 +1,64 @@ +package signer + +import ( + "context" + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("BundleSource", func() { + const ( + trustDomain = "example.org" + // A minimal but well-formed JWKS, shaped like UAA's /token_keys body. + jwks = `{"keys":[{"kty":"RSA","kid":"key-1","alg":"RS256","use":"sig","e":"AQAB","n":"nnnn"}]}` + ) + + It("fetches the UAA JWKS and keys it by the trust domain SPIFFE ID", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer GinkgoRecover() + Expect(r.Method).To(Equal(http.MethodGet)) + Expect(r.URL.Path).To(Equal("/token_keys")) + _, err := w.Write([]byte(jwks)) + Expect(err).NotTo(HaveOccurred()) + })) + defer server.Close() + + bs := NewBundleSource(server.Client(), server.URL+"/token_keys", trustDomain) + bundles, err := bs.Bundles(context.Background()) + + Expect(err).NotTo(HaveOccurred()) + Expect(bundles).To(HaveLen(1)) + Expect(bundles).To(HaveKey("spiffe://example.org")) + // The JWKS bytes are passed through verbatim for go-spiffe clients. + Expect(string(bundles["spiffe://example.org"])).To(Equal(jwks)) + }) + + It("returns an error on a non-2xx response", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + bs := NewBundleSource(server.Client(), server.URL+"/token_keys", trustDomain) + _, err := bs.Bundles(context.Background()) + + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when the body is not a JWKS with keys", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + defer GinkgoRecover() + _, err := w.Write([]byte(`{"not":"a jwks"}`)) + Expect(err).NotTo(HaveOccurred()) + })) + defer server.Close() + + bs := NewBundleSource(server.Client(), server.URL+"/token_keys", trustDomain) + _, err := bs.Bundles(context.Background()) + + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/signer/signer.go b/src/code.cloudfoundry.org/spiffe-agent/signer/signer.go new file mode 100644 index 0000000000..fe8fdd8222 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/signer/signer.go @@ -0,0 +1,125 @@ +// Package signer obtains JWT-SVIDs from the UAA /jwt-svid/sign endpoint, +// proving possession of the workload's instance key over the SPIFFE ID, +// audience, and timestamp. The wire contract is authoritative against Plan A +// (UAA Java): canonical message and key names must byte-match. +package signer + +import ( + "bytes" + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "code.cloudfoundry.org/spiffe-agent/cfattestor" +) + +// Signer exchanges a verified attestation for a JWT-SVID from UAA. +type Signer struct { + httpClient *http.Client + url string + clientID string + clientSecret string + trustDomain string +} + +// New returns a Signer that POSTs to url/jwt-svid/sign with HTTP Basic auth. +func New(c *http.Client, url, clientID, clientSecret, trustDomain string) *Signer { + return &Signer{ + httpClient: c, + url: url, + clientID: clientID, + clientSecret: clientSecret, + trustDomain: trustDomain, + } +} + +type signRequest struct { + InstanceCertificate string `json:"instance_certificate"` + ProcessType string `json:"process_type"` + Audience string `json:"audience"` + Timestamp string `json:"timestamp"` + PopSignature string `json:"pop_signature"` +} + +type signResponse struct { + Svid string `json:"svid"` + SpiffeID string `json:"spiffe_id"` + // ExpiresAt is the JWT-SVID expiry in Unix epoch seconds. UAA (Plan A, + // JwtSvidResponse) serializes this as a JSON number (Java long), so it must + // be decoded as an integer. Informational only: callers read expiry from the + // SVID's own exp claim, so the value is currently unused. + ExpiresAt int64 `json:"expires_at"` +} + +// Sign proves possession of att.InstanceKey over the SPIFFE ID, audience, and a +// single captured timestamp, then exchanges it for a JWT-SVID. The timestamp is +// signed and sent verbatim so UAA can reverify the proof-of-possession. +func (s *Signer) Sign(ctx context.Context, att cfattestor.Attestation, audience string) (svid, spiffeID string, err error) { + spiffeID = cfattestor.BuildSpiffeID(s.trustDomain, att.Selectors) + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + + message := spiffeID + "\n" + audience + "\n" + timestamp + pop, err := signPoP(att.InstanceKey, message) + if err != nil { + return "", "", err + } + + reqBody := signRequest{ + InstanceCertificate: att.CertPEM, + ProcessType: att.Selectors.ProcessType, + Audience: audience, + Timestamp: timestamp, + PopSignature: pop, + } + payload, err := json.Marshal(reqBody) + if err != nil { + return "", "", err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.url+"/jwt-svid/sign", bytes.NewReader(payload)) + if err != nil { + return "", "", err + } + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(s.clientID, s.clientSecret) + + resp, err := s.httpClient.Do(req) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return "", "", fmt.Errorf("jwt-svid sign: status %d: %s", resp.StatusCode, body) + } + + var out signResponse + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return "", "", err + } + return out.Svid, out.SpiffeID, nil +} + +// signPoP produces a STANDARD base64 SHA256withRSA PKCS#1 v1.5 signature over +// message. The key must be RSA. +func signPoP(key crypto.Signer, message string) (string, error) { + if _, ok := key.Public().(*rsa.PublicKey); !ok { + return "", fmt.Errorf("pop: instance key is not RSA") + } + sum := sha256.Sum256([]byte(message)) + sig, err := key.Sign(rand.Reader, sum[:], crypto.SHA256) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(sig), nil +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/signer/signer_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/signer/signer_suite_test.go new file mode 100644 index 0000000000..11571ff79a --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/signer/signer_suite_test.go @@ -0,0 +1,13 @@ +package signer + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSigner(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Signer Suite") +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/signer/signer_test.go b/src/code.cloudfoundry.org/spiffe-agent/signer/signer_test.go new file mode 100644 index 0000000000..aef78f6478 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/signer/signer_test.go @@ -0,0 +1,101 @@ +package signer + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + + "code.cloudfoundry.org/spiffe-agent/cfattestor" + "code.cloudfoundry.org/spiffe-agent/internal/testsupport" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Signer", func() { + const ( + trustDomain = "example.org" + audience = "https://uaa.example.org/oauth/token" + clientID = "spiffe-agent" + clientSecret = "s3cr3t" + // GOLDEN VECTOR: must byte-match Plan A (UAA Java) for these selectors. + goldenSpiffeID = "spiffe://example.org/cf/org/org-1/space/space-2/app/app-3/process/web" + ) + + var ( + att cfattestor.Attestation + pub *rsa.PublicKey + ) + + BeforeEach(func() { + sel := cfattestor.Selectors{OrgID: "org-1", SpaceID: "space-2", AppID: "app-3", ProcessType: "web"} + certPEM, _, key := testsupport.MintInstanceCert("handle-1", sel) + rsaKey, ok := key.(*rsa.PrivateKey) + Expect(ok).To(BeTrue(), "MintInstanceCert must return an RSA signer") + pub = &rsaKey.PublicKey + att = cfattestor.Attestation{Selectors: sel, CertPEM: certPEM, InstanceKey: key} + }) + + It("signs a JWT-SVID with a proof-of-possession that matches the golden canonical message", func() { + var body map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer GinkgoRecover() + Expect(r.Method).To(Equal(http.MethodPost)) + Expect(r.URL.Path).To(Equal("/jwt-svid/sign")) + u, p, ok := r.BasicAuth() + Expect(ok).To(BeTrue()) + Expect(u).To(Equal(clientID)) + Expect(p).To(Equal(clientSecret)) + + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + Expect(body).To(HaveKey("instance_certificate")) + Expect(body).To(HaveKey("process_type")) + Expect(body).To(HaveKey("audience")) + Expect(body).To(HaveKey("timestamp")) + Expect(body).To(HaveKey("pop_signature")) + Expect(body["instance_certificate"]).To(Equal(att.CertPEM)) + Expect(body["process_type"]).To(Equal("web")) + Expect(body["audience"]).To(Equal(audience)) + + // GOLDEN: canonical PoP message = spiffeID\naudience\ntimestamp. + message := goldenSpiffeID + "\n" + audience + "\n" + body["timestamp"] + sig, err := base64.StdEncoding.DecodeString(body["pop_signature"]) + Expect(err).NotTo(HaveOccurred()) + sum := sha256.Sum256([]byte(message)) + Expect(rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig)).To(Succeed()) + + // UAA (Plan A) returns expires_at as a JSON number (epoch seconds). + // Emit raw JSON so this test pins the wire contract independently of + // signResponse's Go field types — this exact shape regressed e2e. + _, writeErr := w.Write([]byte(`{"svid":"jwt","spiffe_id":"` + goldenSpiffeID + `","expires_at":1782691200}`)) + Expect(writeErr).NotTo(HaveOccurred()) + })) + defer server.Close() + + s := New(server.Client(), server.URL, clientID, clientSecret, trustDomain) + svid, spiffeID, err := s.Sign(context.Background(), att, audience) + + Expect(err).NotTo(HaveOccurred()) + Expect(svid).To(Equal("jwt")) + Expect(spiffeID).To(Equal(goldenSpiffeID)) + // The signed message used the same timestamp sent in the request body. + Expect(body["timestamp"]).NotTo(BeEmpty()) + }) + + It("returns an error on a non-2xx response", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + s := New(server.Client(), server.URL, clientID, clientSecret, trustDomain) + _, _, err := s.Sign(context.Background(), att, audience) + + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/workloadapi/peercred.go b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/peercred.go new file mode 100644 index 0000000000..e05819ca32 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/peercred.go @@ -0,0 +1,78 @@ +package workloadapi + +import ( + "context" + "errors" + "net" + "syscall" + + "golang.org/x/sys/unix" + "google.golang.org/grpc/credentials" +) + +// peerCredentials implements credentials.TransportCredentials by reading the +// SO_PEERCRED socket option of the connecting Unix-domain peer. The verified +// pid/uid/gid are exposed via peerCredAuthInfo on the connection context. +type peerCredentials struct{} + +// peerCredAuthInfo carries the kernel-verified peer credentials for a +// connection. SecurityLevel is PrivacyAndIntegrity because a local Unix socket +// pair is not observable by other parties. +type peerCredAuthInfo struct { + credentials.CommonAuthInfo + PID int32 + UID int32 + GID int32 +} + +// AuthType identifies this AuthInfo so callers can type-assert it. +func (peerCredAuthInfo) AuthType() string { return "peercred" } + +// ServerHandshake reads SO_PEERCRED from the underlying socket and attaches the +// peer's pid/uid/gid to the returned AuthInfo. The connection is passed through +// unchanged. +func (peerCredentials) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + sc, ok := conn.(syscall.Conn) + if !ok { + return nil, nil, errors.New("workloadapi: connection does not support SyscallConn") + } + raw, err := sc.SyscallConn() + if err != nil { + return nil, nil, err + } + + var ucred *unix.Ucred + var ctrlErr error + if err := raw.Control(func(fd uintptr) { + ucred, ctrlErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + }); err != nil { + return nil, nil, err + } + if ctrlErr != nil { + return nil, nil, ctrlErr + } + + info := peerCredAuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}, + PID: ucred.Pid, + UID: int32(ucred.Uid), + GID: int32(ucred.Gid), + } + return conn, info, nil +} + +// ClientHandshake is unused: these credentials only authenticate local servers. +func (peerCredentials) ClientHandshake(context.Context, string, net.Conn) (net.Conn, credentials.AuthInfo, error) { + return nil, nil, errors.New("workloadapi: ClientHandshake not implemented") +} + +// Info reports a plaintext protocol; security comes from the peer credentials. +func (peerCredentials) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{SecurityProtocol: "peercred"} +} + +// Clone returns a copy; peerCredentials is stateless. +func (peerCredentials) Clone() credentials.TransportCredentials { return peerCredentials{} } + +// OverrideServerName is a no-op for Unix-socket peer credentials. +func (peerCredentials) OverrideServerName(string) error { return nil } diff --git a/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server.go b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server.go new file mode 100644 index 0000000000..8c1d1aa9e5 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server.go @@ -0,0 +1,204 @@ +package workloadapi + +import ( + "bytes" + "context" + "net" + "os" + "time" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/spiffe-agent/cfattestor" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/tedsuo/ifrit" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +const securityHeader = "workload.spiffe.io" + +// Attestor turns a peer pid into a verified Cloud Foundry workload attestation. +type Attestor interface { + Attest(ctx context.Context, pid int) (cfattestor.Attestation, error) +} + +// Signer exchanges an attestation for a signed JWT-SVID and its SPIFFE ID. +type Signer interface { + Sign(ctx context.Context, att cfattestor.Attestation, audience string) (svid, spiffeID string, err error) +} + +// BundleSource provides the current JWT trust bundles (JWKS documents) keyed by +// trust domain SPIFFE ID, for the FetchJWTBundles RPC. +type BundleSource interface { + Bundles(ctx context.Context) (map[string][]byte, error) +} + +// Server implements the SPIFFE Workload API, issuing JWT-SVIDs to the local +// process authenticated via SO_PEERCRED. +type Server struct { + workload.UnimplementedSpiffeWorkloadAPIServer + attestor Attestor + signer Signer + bundles BundleSource + refreshInterval time.Duration +} + +// NewServer wires an attestor, signer, and JWT bundle source into a Workload API +// server. refreshInterval controls how often FetchJWTBundles polls the bundle +// source for rotations. +func NewServer(attestor Attestor, signer Signer, bundles BundleSource, refreshInterval time.Duration) *Server { + return &Server{ + attestor: attestor, + signer: signer, + bundles: bundles, + refreshInterval: refreshInterval, + } +} + +// FetchJWTSVID attests the calling pid and returns a JWT-SVID for the first +// requested audience. +func (s *Server) FetchJWTSVID(ctx context.Context, req *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { + if err := checkSecurityHeader(ctx); err != nil { + return nil, err + } + if len(req.Audience) < 1 { + return nil, status.Error(codes.InvalidArgument, "audience is required") + } + + p, ok := peer.FromContext(ctx) + if !ok { + return nil, status.Error(codes.Internal, "no peer information in context") + } + authInfo, ok := p.AuthInfo.(peerCredAuthInfo) + if !ok { + return nil, status.Error(codes.Unauthenticated, "missing peer credentials") + } + + att, err := s.attestor.Attest(ctx, int(authInfo.PID)) + if err != nil { + return nil, status.Errorf(codes.PermissionDenied, "attestation failed: %v", err) + } + svid, spiffeID, err := s.signer.Sign(ctx, att, req.Audience[0]) + if err != nil { + return nil, status.Errorf(codes.Internal, "signing failed: %v", err) + } + + return &workload.JWTSVIDResponse{ + Svids: []*workload.JWTSVID{{SpiffeId: spiffeID, Svid: svid}}, + }, nil +} + +// FetchJWTBundles streams the JWT trust bundles (JWKS documents), keyed by trust +// domain SPIFFE ID. It sends the current bundles immediately, then re-sends +// whenever they change, polling the bundle source at the configured refresh +// interval to pick up signing-key rotation. The stream stays open until the +// client disconnects. Bundles are public, so no peer attestation is performed; +// only the SPIFFE security header is required. +func (s *Server) FetchJWTBundles(_ *workload.JWTBundlesRequest, stream workload.SpiffeWorkloadAPI_FetchJWTBundlesServer) error { + ctx := stream.Context() + if err := checkSecurityHeader(ctx); err != nil { + return err + } + + last, err := s.bundles.Bundles(ctx) + if err != nil { + return status.Errorf(codes.Internal, "fetch bundles: %v", err) + } + if err := stream.Send(&workload.JWTBundlesResponse{Bundles: last}); err != nil { + return err + } + + ticker := time.NewTicker(s.refreshInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + current, err := s.bundles.Bundles(ctx) + if err != nil { + // Transient fetch failure: keep streaming the last good bundle + // rather than tearing down the client's stream. + continue + } + if sameBundles(current, last) { + continue + } + last = current + if err := stream.Send(&workload.JWTBundlesResponse{Bundles: current}); err != nil { + return err + } + } + } +} + +// sameBundles reports whether two bundle maps have identical keys and bytes. +func sameBundles(a, b map[string][]byte) bool { + if len(a) != len(b) { + return false + } + for k, av := range a { + bv, ok := b[k] + if !ok || !bytes.Equal(av, bv) { + return false + } + } + return true +} + +func checkSecurityHeader(ctx context.Context) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return status.Error(codes.InvalidArgument, "missing metadata") + } + for _, v := range md.Get(securityHeader) { + if v == "true" { + return nil + } + } + return status.Errorf(codes.InvalidArgument, "missing %q header", securityHeader) +} + +// NewRunner returns an ifrit.Runner that serves the Workload API over a Unix +// socket guarded by SO_PEERCRED peer credentials, shutting down gracefully on +// signal. +func NewRunner(socketPath string, srv *Server, logger lager.Logger) ifrit.Runner { + return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { + logger = logger.Session("workloadapi") + + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + return err + } + listener, err := net.Listen("unix", socketPath) + if err != nil { + return err + } + if err := os.Chmod(socketPath, 0666); err != nil { + listener.Close() + return err + } + + grpcServer := grpc.NewServer(grpc.Creds(peerCredentials{})) + workload.RegisterSpiffeWorkloadAPIServer(grpcServer, srv) + + serveErr := make(chan error, 1) + go func() { + serveErr <- grpcServer.Serve(listener) + }() + + logger.Info("listening", lager.Data{"socket": socketPath}) + close(ready) + + select { + case err := <-serveErr: + return err + case <-signals: + logger.Info("stopping") + grpcServer.GracefulStop() + return nil + } + }) +} diff --git a/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server_test.go b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server_test.go new file mode 100644 index 0000000000..43c26c080b --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/server_test.go @@ -0,0 +1,159 @@ +package workloadapi_test + +import ( + "context" + "os" + "path/filepath" + "sync" + "time" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/spiffe-agent/cfattestor" + "code.cloudfoundry.org/spiffe-agent/workloadapi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/ginkgomon_v2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +type fakeAttestor struct { + mu sync.Mutex + gotPID int + att cfattestor.Attestation + err error +} + +func (f *fakeAttestor) Attest(_ context.Context, pid int) (cfattestor.Attestation, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.gotPID = pid + return f.att, f.err +} + +type fakeSigner struct { + mu sync.Mutex + gotAudience string + svid string + spiffeID string + err error +} + +func (f *fakeSigner) Sign(_ context.Context, _ cfattestor.Attestation, audience string) (string, string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.gotAudience = audience + return f.svid, f.spiffeID, f.err +} + +type fakeBundleSource struct { + mu sync.Mutex + bundles map[string][]byte + err error +} + +func (f *fakeBundleSource) set(bundles map[string][]byte) { + f.mu.Lock() + defer f.mu.Unlock() + f.bundles = bundles +} + +func (f *fakeBundleSource) Bundles(_ context.Context) (map[string][]byte, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.bundles, f.err +} + +var _ = Describe("WorkloadAPI Server", func() { + var ( + attestor *fakeAttestor + signer *fakeSigner + bundleSource *fakeBundleSource + sock string + process ifrit.Process + client workload.SpiffeWorkloadAPIClient + conn *grpc.ClientConn + ) + + BeforeEach(func() { + attestor = &fakeAttestor{att: cfattestor.Attestation{CertPEM: "canned"}} + signer = &fakeSigner{svid: "jwt", spiffeID: "spiffe://example.org/workload/foo"} + bundleSource = &fakeBundleSource{bundles: map[string][]byte{"spiffe://example.org": []byte("jwks-1")}} + + sock = filepath.Join(GinkgoT().TempDir(), "agent.sock") + srv := workloadapi.NewServer(attestor, signer, bundleSource, 20*time.Millisecond) + runner := workloadapi.NewRunner(sock, srv, lagertest.NewTestLogger("workloadapi")) + process = ginkgomon_v2.Invoke(runner) + + var err error + conn, err = grpc.NewClient("unix://"+sock, grpc.WithTransportCredentials(insecure.NewCredentials())) + Expect(err).NotTo(HaveOccurred()) + client = workload.NewSpiffeWorkloadAPIClient(conn) + }) + + AfterEach(func() { + if conn != nil { + conn.Close() + } + ginkgomon_v2.Kill(process) + }) + + withHeader := func() context.Context { + return metadata.AppendToOutgoingContext(context.Background(), "workload.spiffe.io", "true") + } + + It("issues a JWT-SVID from the attested PID", func() { + resp, err := client.FetchJWTSVID(withHeader(), &workload.JWTSVIDRequest{Audience: []string{"foo"}}) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Svids).To(HaveLen(1)) + Expect(resp.Svids[0].Svid).To(Equal("jwt")) + Expect(resp.Svids[0].SpiffeId).To(Equal("spiffe://example.org/workload/foo")) + Expect(signer.gotAudience).To(Equal("foo")) + Expect(attestor.gotPID).To(Equal(os.Getpid())) + }) + + It("rejects requests missing the security header", func() { + _, err := client.FetchJWTSVID(context.Background(), &workload.JWTSVIDRequest{Audience: []string{"foo"}}) + Expect(err).To(HaveOccurred()) + }) + + It("rejects requests with no audience", func() { + _, err := client.FetchJWTSVID(withHeader(), &workload.JWTSVIDRequest{Audience: nil}) + Expect(err).To(HaveOccurred()) + }) + + It("streams the current JWT bundles to an authenticated client", func() { + stream, err := client.FetchJWTBundles(withHeader(), &workload.JWTBundlesRequest{}) + Expect(err).NotTo(HaveOccurred()) + + resp, err := stream.Recv() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Bundles).To(HaveKeyWithValue("spiffe://example.org", []byte("jwks-1"))) + }) + + It("rejects bundle requests missing the security header", func() { + stream, err := client.FetchJWTBundles(context.Background(), &workload.JWTBundlesRequest{}) + Expect(err).NotTo(HaveOccurred()) + + _, err = stream.Recv() + Expect(err).To(HaveOccurred()) + }) + + It("re-sends the JWT bundles when they rotate", func() { + stream, err := client.FetchJWTBundles(withHeader(), &workload.JWTBundlesRequest{}) + Expect(err).NotTo(HaveOccurred()) + + first, err := stream.Recv() + Expect(err).NotTo(HaveOccurred()) + Expect(first.Bundles).To(HaveKeyWithValue("spiffe://example.org", []byte("jwks-1"))) + + bundleSource.set(map[string][]byte{"spiffe://example.org": []byte("jwks-2")}) + + second, err := stream.Recv() + Expect(err).NotTo(HaveOccurred()) + Expect(second.Bundles).To(HaveKeyWithValue("spiffe://example.org", []byte("jwks-2"))) + }) +}) diff --git a/src/code.cloudfoundry.org/spiffe-agent/workloadapi/workloadapi_suite_test.go b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/workloadapi_suite_test.go new file mode 100644 index 0000000000..21f0c9fca4 --- /dev/null +++ b/src/code.cloudfoundry.org/spiffe-agent/workloadapi/workloadapi_suite_test.go @@ -0,0 +1,13 @@ +package workloadapi_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestWorkloadAPI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "WorkloadAPI Suite") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/db_health_check_runner.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/db_health_check_runner.go new file mode 100644 index 0000000000..e0726a0c5a --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/db_health_check_runner.go @@ -0,0 +1,139 @@ +// @AI-Generated +// Generated in whole or in part by Cursor with a mix of different LLM models (Auto select mode) +// Description: +// 2026-03-23: Cancel DB health check context on timeout to release row locks + +package main + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "time" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +type DBHealthCheckRunner struct { + logger lager.Logger + sqlDB db.BBSHealthCheckDB + clock clock.Clock + lock sync.Mutex + isRunning bool + migrationsDone chan struct{} + HealthCheckFailureThreshold int + HealthCheckTimeout time.Duration + HealthCheckInterval time.Duration +} + +func NewDBHealthCheckRunner(logger lager.Logger, sqlDB db.BBSHealthCheckDB, clock clock.Clock, failureCount int, timeout, interval time.Duration, migrationsDone chan struct{}) *DBHealthCheckRunner { + if failureCount == 0 { + failureCount = 3 + } + if timeout == 0 { + timeout = 5 * time.Second + } + if interval == 0 { + interval = 10 * time.Second + } + return &DBHealthCheckRunner{ + logger: logger.Session("db-health-check-runner"), + sqlDB: sqlDB, + clock: clock, + HealthCheckFailureThreshold: failureCount, + HealthCheckTimeout: timeout, + HealthCheckInterval: interval, + migrationsDone: migrationsDone, + lock: sync.Mutex{}, + } +} + +func (runner *DBHealthCheckRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + close(ready) + runner.logger.Info("waiting-for-db-migrations") + select { + case <-runner.migrationsDone: + case <-signals: + runner.logger.Debug("exiting-due-to-signal") + return nil + } + runner.logger.Info("starting") + defer runner.logger.Info("exiting") + ticker := runner.clock.NewTicker(runner.HealthCheckInterval) + healthCheckResults := make(chan error) + for { + runner.logger.Debug("reentering-run-loop") + select { + case err := <-healthCheckResults: + runner.lock.Lock() + runner.isRunning = false + runner.lock.Unlock() + if err != nil { + runner.logger.Error("database-failure-detected-restarting-bbs", err) + return err + } + runner.logger.Debug("health-check-succeeded") + case <-signals: + runner.logger.Debug("exiting-due-to-signal") + return nil + case <-ticker.C(): + runner.lock.Lock() + if !runner.isRunning { + runner.isRunning = true + runner.logger.Debug("executing-health-check") + go runner.ExecuteTimedHealthCheckWithRetries(healthCheckResults) + } + runner.lock.Unlock() + } + } +} + +func (runner *DBHealthCheckRunner) ExecuteTimedHealthCheckWithRetries(resultChan chan error) { + var errs []error + for i := 1; i <= runner.HealthCheckFailureThreshold; i++ { + logger := runner.logger.WithData(lager.Data{"attempt": i}) + logger.Debug("executing-timed-health-check") + err := runner.ExecuteTimedHealthCheck() + if err != nil { + logger.Error("failed-health-check", err) + errs = append(errs, err) + } else { + resultChan <- nil + return + } + } + finalErr := errors.Join(errs...) + runner.logger.Error("health-check-attempts-exceeded", finalErr, lager.Data{"max-attempts": runner.HealthCheckFailureThreshold}) + resultChan <- finalErr +} + +func (runner *DBHealthCheckRunner) ExecuteTimedHealthCheck() error { + timer := runner.clock.NewTimer(runner.HealthCheckTimeout) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + errChan := make(chan error) + go runner.runDBHealthCheck(ctx, errChan) + + select { + case err := <-errChan: + if err == nil { + return nil + } else { + return err + } + case <-timer.C(): + cancel() + err := fmt.Errorf("timed out after %s while executing DB health check", runner.HealthCheckTimeout) + runner.logger.Error("health-check-timed-out", err) + return err + } +} + +func (runner *DBHealthCheckRunner) runDBHealthCheck(ctx context.Context, errChan chan error) { + err := runner.sqlDB.PerformBBSHealthCheck(ctx, runner.logger, time.Now()) + errChan <- err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/main.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/main.go new file mode 100644 index 0000000000..861358ad66 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/main.go @@ -0,0 +1,475 @@ +package main + +import ( + "context" + "crypto/rand" + "errors" + "flag" + "fmt" + "net" + "net/http" + "os" + "time" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/cmd/bbs/config" + "code.cloudfoundry.org/bbs/controllers" + "code.cloudfoundry.org/bbs/converger" + "code.cloudfoundry.org/bbs/db/migrations" + "code.cloudfoundry.org/bbs/db/sqldb" + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/encryptor" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/handlers" + "code.cloudfoundry.org/bbs/metrics" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/taskworkpool" + cfhttp "code.cloudfoundry.org/cfhttp/v2" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/debugserver" + "code.cloudfoundry.org/diego-db-helpers/guidprovider" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers/monitor" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/go-loggregator/v9/runtimeemitter" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagerflags" + "code.cloudfoundry.org/locket" + "code.cloudfoundry.org/locket/jointlock" + "code.cloudfoundry.org/locket/lock" + "code.cloudfoundry.org/locket/lockheldmetrics" + locketmodels "code.cloudfoundry.org/locket/models" + "code.cloudfoundry.org/rep" + "code.cloudfoundry.org/tlsconfig" + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/grouper" + "github.com/tedsuo/ifrit/http_server" + "github.com/tedsuo/ifrit/sigmon" +) + +var configFilePath = flag.String( + "config", + "", + "The path to the JSON configuration file.", +) + +const ( + bbsLockKey = "bbs" +) + +func main() { + flag.Parse() + + bbsConfig, err := config.NewBBSConfig(*configFilePath) + if err != nil { + panic(err.Error()) + } + + logger, reconfigurableSink := lagerflags.NewFromConfig(bbsConfig.SessionName, bbsConfig.LagerConfig) + logger.Info("starting") + + metronClient, err := initializeMetron(logger, bbsConfig) + if err != nil { + logger.Error("failed-to-initialize-metron-client", err) + os.Exit(1) + } + + clock := clock.NewClock() + + _, portString, err := net.SplitHostPort(bbsConfig.HealthAddress) + if err != nil { + logger.Fatal("failed-invalid-health-address", err) + } + _, err = net.LookupPort("tcp", portString) + if err != nil { + logger.Fatal("failed-invalid-health-port", err) + } + + key, keys, err := bbsConfig.EncryptionConfig.Parse() + if err != nil { + logger.Fatal("cannot-setup-encryption", err) + } + keyManager, err := encryption.NewKeyManager(key, keys) + if err != nil { + logger.Fatal("cannot-setup-encryption", err) + } + cryptor := encryption.NewCryptor(keyManager, rand.Reader) + + if bbsConfig.DatabaseDriver == "" || bbsConfig.DatabaseConnectionString == "" { + logger.Fatal("no-database-configured", errors.New("no database configured")) + } + + params := &helpers.ConnectParams{ + DriverName: bbsConfig.DatabaseDriver, + DatabaseConnectionString: bbsConfig.DatabaseConnectionString, + SqlCACertFile: bbsConfig.SQLCACertFile, + SqlEnableIdentityVerification: bbsConfig.SQLEnableIdentityVerification, + ConnectionTimeout: time.Duration(bbsConfig.DBConnectionTimeout), + ReadTimeout: time.Duration(bbsConfig.DBReadTimeout), + WriteTimeout: time.Duration(bbsConfig.DBWriteTimeout), + } + sqlConn, err := helpers.Connect( + logger, + params, + ) + if err != nil { + logger.Fatal("failed-to-open-sql", err) + } + defer sqlConn.Close() + + sqlConn.SetMaxOpenConns(bbsConfig.MaxOpenDatabaseConnections) + sqlConn.SetMaxIdleConns(bbsConfig.MaxIdleDatabaseConnections) + sqlConn.SetConnMaxLifetime(time.Duration(bbsConfig.MaxDatabaseConnectionLifetime)) + + err = sqlConn.Ping() + if err != nil { + logger.Fatal("sql-failed-to-connect", err) + } + + queryMonitor := monitor.New() + monitoredDB := helpers.NewMonitoredDB(sqlConn, queryMonitor) + sqlDB := sqldb.NewSQLDB( + monitoredDB, + bbsConfig.ConvergenceWorkers, + bbsConfig.UpdateWorkers, + cryptor, + guidprovider.DefaultGuidProvider, + clock, + bbsConfig.DatabaseDriver, + metronClient, + bbsConfig.DebugLRPStartHeartbeats, + ) + err = sqlDB.CreateConfigurationsTable(context.Background(), logger) + if err != nil { + logger.Fatal("sql-failed-create-configurations-table", err) + } + + encryptor := encryptor.New(logger, sqlDB, keyManager, cryptor, clock, metronClient) + + migrationsDone := make(chan struct{}) + + migrationManager := migration.NewManager( + logger, + sqlDB, + sqlConn, + cryptor, + migrations.AllMigrations(), + migrationsDone, + clock, + bbsConfig.DatabaseDriver, + metronClient, + ) + + dbHealthCheckRunner := NewDBHealthCheckRunner(logger, sqlDB, clock, bbsConfig.HealthCheckFailureThreshold, time.Duration(bbsConfig.HealthCheckTimeout), time.Duration(bbsConfig.HealthCheckInterval), migrationsDone) + + desiredHub := events.NewHub(logger) + actualHub := events.NewHub(logger) + actualLRPInstanceHub := events.NewHub(logger) + taskHub := events.NewHub(logger) + + repTLSConfig := &rep.TLSConfig{ + RequireTLS: true, + CaCertFile: bbsConfig.RepCACert, + CertFile: bbsConfig.RepClientCert, + KeyFile: bbsConfig.RepClientKey, + ClientCacheSize: bbsConfig.RepClientSessionCacheSize, + } + + httpClient := cfhttp.NewClient( + cfhttp.WithRequestTimeout(time.Duration(bbsConfig.CommunicationTimeout)), + ) + repClientFactory, err := rep.NewClientFactory(httpClient, httpClient, repTLSConfig) + if err != nil { + logger.Fatal("new-rep-client-factory-failed", err) + } + + auctioneerClient := initializeAuctioneerClient(logger, &bbsConfig) + + exitChan := make(chan struct{}) + + var accessLogger lager.Logger + if bbsConfig.AccessLogPath != "" { + accessLogger = lager.NewLogger("bbs-access") + file, err := os.OpenFile(bbsConfig.AccessLogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + logger.Error("invalid-access-log-path", err, lager.Data{"access-log-path": bbsConfig.AccessLogPath}) + os.Exit(1) + } + accessLogger.RegisterSink(lager.NewWriterSink(file, lager.INFO)) + } + + tlsConfig, err := tlsconfig.Build( + tlsconfig.WithInternalServiceDefaults(), + tlsconfig.WithIdentityFromFile(bbsConfig.CertFile, bbsConfig.KeyFile), + ).Server(tlsconfig.WithClientAuthenticationFromFile(bbsConfig.CaFile)) + if err != nil { + logger.Fatal("tls-configuration-failed", err) + } + // the BBS server performs requests as a client + if tlsConfig.ClientCAs != nil { + tlsConfig.RootCAs = tlsConfig.ClientCAs + } + + cbWorkPool := taskworkpool.New(logger, + bbsConfig.TaskCallbackWorkers, + taskworkpool.HandleCompletedTask, + tlsConfig, + time.Duration(bbsConfig.CommunicationTimeout)) + + locks := []grouper.Member{} + + var locketClient locketmodels.LocketClient + locketClient, err = locket.NewClient(logger, bbsConfig.ClientLocketConfig) + if err != nil { + logger.Fatal("failed-to-create-locket-client", err) + } + + if bbsConfig.UUID == "" { + logger.Fatal("invalid-uuid", errors.New("invalid-uuid-from-config")) + } + + lockIdentifier := &locketmodels.Resource{ + Key: bbsLockKey, + Owner: bbsConfig.UUID, + TypeCode: locketmodels.LOCK, + Type: locketmodels.LockType, + } + + locks = append(locks, grouper.Member{Name: "sql-lock", Runner: lock.NewLockRunner( + logger, + locketClient, + lockIdentifier, + int64(time.Duration(bbsConfig.LockTTL)/time.Second), + clock, + time.Duration(bbsConfig.LockRetryInterval), + )}) + + var lock ifrit.Runner + switch len(locks) { + case 0: + logger.Fatal("no-locks-configured", errors.New("lock configuration must be provided")) + case 1: + lock = locks[0] + default: + lock = jointlock.NewJointLock(clock, 2*time.Second, locks...) + } + + serviceClient := serviceclient.NewServiceClient(locketClient, time.Duration(bbsConfig.DBConnectionTimeout)) + + logger.Info("report-interval", lager.Data{"value": bbsConfig.ReportInterval}) + fileDescriptorTicker := clock.NewTicker(time.Duration(bbsConfig.ReportInterval)) + requestStatsTicker := clock.NewTicker(time.Duration(bbsConfig.ReportInterval)) + locksHeldTicker := clock.NewTicker(time.Duration(bbsConfig.ReportInterval)) + + fileDescriptorPath := fmt.Sprintf("/proc/%d/fd", os.Getpid()) + fileDescriptorMetronNotifier := metrics.NewFileDescriptorMetronNotifier(logger, fileDescriptorTicker, metronClient, fileDescriptorPath) + requestStatMetronNotifier := metrics.NewRequestStatMetronNotifier(logger, requestStatsTicker, metronClient, bbsConfig.AdvancedMetricsConfig) + lockHeldMetronNotifier := lockheldmetrics.NewLockHeldMetronNotifier(logger, locksHeldTicker, metronClient) + taskStatMetronNotifier := metrics.NewTaskStatMetronNotifier(logger, clock, metronClient) + dbStatMetronNotifier := metrics.NewDBStatMetronNotifier(logger, clock, monitoredDB, metronClient, queryMonitor) + + handler := handlers.New( + logger, + accessLogger, + bbsConfig.UpdateWorkers, + bbsConfig.ConvergenceWorkers, + bbsConfig.MaxTaskRetries, + bbsConfig.AdvancedMetricsConfig, + requestStatMetronNotifier, + sqlDB, + desiredHub, + actualHub, + actualLRPInstanceHub, + taskHub, + cbWorkPool, + serviceClient, + auctioneerClient, + repClientFactory, + taskStatMetronNotifier, + migrationsDone, + exitChan, + metronClient, + ) + + bbsElectionMetronNotifier := metrics.NewBBSElectionMetronNotifier(logger, metronClient) + + actualLRPController := controllers.NewActualLRPLifecycleController( + sqlDB, + sqlDB, + sqlDB, + sqlDB, + auctioneerClient, + serviceClient, + repClientFactory, + actualHub, + actualLRPInstanceHub, + ) + + lrpStatMetronNotifier := metrics.NewLRPStatMetronNotifier(logger, clock, metronClient) + + lrpConvergenceController := controllers.NewLRPConvergenceController( + logger, + clock, + sqlDB, + sqlDB, + sqlDB, + actualHub, + actualLRPInstanceHub, + auctioneerClient, + serviceClient, + repClientFactory, + actualLRPController, + bbsConfig.ConvergenceWorkers, + lrpStatMetronNotifier, + ) + + taskController := controllers.NewTaskController( + sqlDB, + cbWorkPool, + auctioneerClient, + serviceClient, + repClientFactory, + taskHub, + taskStatMetronNotifier, + bbsConfig.MaxTaskRetries, + ) + + convergerProcess := converger.New( + logger, + clock, + lrpConvergenceController, + taskController, + serviceClient, + time.Duration(bbsConfig.ConvergeRepeatInterval), + time.Duration(bbsConfig.KickTaskDuration), + time.Duration(bbsConfig.ExpirePendingTaskDuration), + time.Duration(bbsConfig.ExpireCompletedTaskDuration), + ) + + var server ifrit.Runner + if tlsConfig != nil { + server = http_server.NewTLSServer(bbsConfig.ListenAddress, handler, tlsConfig) + } else { + server = http_server.New(bbsConfig.ListenAddress, handler) + } + + healthcheckServer := http_server.New(bbsConfig.HealthAddress, http.HandlerFunc(healthCheckHandler)) + + members := grouper.Members{ + {Name: "healthcheck", Runner: healthcheckServer}, + {Name: "periodic-filedescriptor-metrics", Runner: fileDescriptorMetronNotifier}, + {Name: "lock-held-metrics", Runner: lockHeldMetronNotifier}, + {Name: "lock", Runner: lock}, + {Name: "set-lock-held-metrics", Runner: lockheldmetrics.SetLockHeldRunner(logger, *lockHeldMetronNotifier)}, + {Name: "workpool", Runner: cbWorkPool}, + {Name: "server", Runner: server}, + {Name: "migration-manager", Runner: migrationManager}, + {Name: "encryptor", Runner: encryptor}, + {Name: "hub-maintainer", Runner: hubMaintainer(logger, desiredHub, actualHub, taskHub)}, + {Name: "bbs-election-metrics", Runner: bbsElectionMetronNotifier}, + {Name: "periodic-metrics", Runner: requestStatMetronNotifier}, + {Name: "converger", Runner: convergerProcess}, + {Name: "lrp-stat-metron-notifier", Runner: lrpStatMetronNotifier}, + {Name: "task-stat-metron-notifier", Runner: taskStatMetronNotifier}, + {Name: "db-stat-metron-notifier", Runner: dbStatMetronNotifier}, + } + + if bbsConfig.EnableDBHealthCheck { + members = append(grouper.Members{{Name: "db-healthcheck", Runner: dbHealthCheckRunner}}, members...) + } + + if bbsConfig.DebugAddress != "" { + members = append(grouper.Members{ + {Name: "debug-server", Runner: debugserver.Runner(bbsConfig.DebugAddress, reconfigurableSink)}, + }, members...) + } + + group := grouper.NewOrdered(os.Interrupt, members) + + monitor := ifrit.Invoke(sigmon.New(group)) + go func() { + // If a handler writes to this channel, we've hit an unrecoverable error + // and should shut down (cleanly) + <-exitChan + monitor.Signal(os.Interrupt) + }() + + logger.Info("started") + + err = <-monitor.Wait() + if sqlConn != nil { + closeErr := sqlConn.Close() + if closeErr != nil { + logger.Error("failed-to-close-sql-conn", closeErr) + } + } + if err != nil { + logger.Error("exited-with-failure", err) + os.Exit(1) + } + + logger.Info("exited") +} + +func healthCheckHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func hubMaintainer(logger lager.Logger, desiredHub, actualHub, taskHub events.Hub) ifrit.RunFunc { + return func(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := logger.Session("hub-maintainer") + close(ready) + logger.Info("started") + defer logger.Info("finished") + + <-signals + err := desiredHub.Close() + if err != nil { + logger.Error("error-closing-desired-hub", err) + } + err = actualHub.Close() + if err != nil { + logger.Error("error-closing-actual-hub", err) + } + err = taskHub.Close() + if err != nil { + logger.Error("error-closing-actual-hub", err) + } + return nil + } +} + +func initializeAuctioneerClient(logger lager.Logger, bbsConfig *config.BBSConfig) auctioneer.Client { + if bbsConfig.AuctioneerAddress == "" { + logger.Fatal("auctioneer-address-validation-failed", errors.New("auctioneerAddress is required")) + } + + if bbsConfig.AuctioneerCACert != "" || bbsConfig.AuctioneerClientCert != "" || bbsConfig.AuctioneerClientKey != "" { + client, err := auctioneer.NewSecureClient(bbsConfig.AuctioneerAddress, + bbsConfig.AuctioneerCACert, + bbsConfig.AuctioneerClientCert, + bbsConfig.AuctioneerClientKey, + bbsConfig.AuctioneerRequireTLS, + time.Duration(bbsConfig.CommunicationTimeout), + ) + if err != nil { + logger.Fatal("failed-to-construct-auctioneer-client", err) + } + return client + } + + return auctioneer.NewClient(bbsConfig.AuctioneerAddress, time.Duration(bbsConfig.CommunicationTimeout)) +} + +func initializeMetron(logger lager.Logger, bbsConfig config.BBSConfig) (loggingclient.IngressClient, error) { + client, err := loggingclient.NewIngressClient(bbsConfig.LoggregatorConfig) + if err != nil { + return nil, err + } + + emitter := runtimeemitter.NewV1(client) + go emitter.Run() + + return client, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/package.go new file mode 100644 index 0000000000..f05696d5a1 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/cmd/bbs/package.go @@ -0,0 +1 @@ +package main // import "code.cloudfoundry.org/bbs/cmd/bbs" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/actual_lrp_lifecycle_controller.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/actual_lrp_lifecycle_controller.go new file mode 100644 index 0000000000..731a2a7127 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/actual_lrp_lifecycle_controller.go @@ -0,0 +1,348 @@ +package controllers + +import ( + "context" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/events/calculator" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/rep" +) + +type ActualLRPLifecycleController struct { + db db.ActualLRPDB + suspectDB db.SuspectDB + evacuationDB db.EvacuationDB + desiredLRPDB db.DesiredLRPDB + auctioneerClient auctioneer.Client + serviceClient serviceclient.ServiceClient + repClientFactory rep.ClientFactory + actualHub events.Hub + actualLRPInstanceHub events.Hub +} + +func NewActualLRPLifecycleController( + db db.ActualLRPDB, + suspectDB db.SuspectDB, + evacuationDB db.EvacuationDB, + desiredLRPDB db.DesiredLRPDB, + auctioneerClient auctioneer.Client, + serviceClient serviceclient.ServiceClient, + repClientFactory rep.ClientFactory, + actualHub events.Hub, + actualLRPInstanceHub events.Hub, +) *ActualLRPLifecycleController { + return &ActualLRPLifecycleController{ + db: db, + suspectDB: suspectDB, + evacuationDB: evacuationDB, + desiredLRPDB: desiredLRPDB, + auctioneerClient: auctioneerClient, + serviceClient: serviceClient, + repClientFactory: repClientFactory, + actualHub: actualHub, + actualLRPInstanceHub: actualLRPInstanceHub, + } +} + +func findWithPresence(lrps []*models.ActualLRP, presence models.ActualLRP_Presence) *models.ActualLRP { + for _, lrp := range lrps { + if lrp.Presence == presence { + return lrp + } + } + return nil +} + +func lookupLRPInSlice(lrps []*models.ActualLRP, key *models.ActualLRPInstanceKey) *models.ActualLRP { + for _, lrp := range lrps { + if lrp.ActualLRPInstanceKey == *key { + return lrp + } + } + return nil +} + +func (h *ActualLRPLifecycleController) ClaimActualLRP(ctx context.Context, logger lager.Logger, processGUID string, index int32, actualLRPInstanceKey *models.ActualLRPInstanceKey) error { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + lrps, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: processGUID, Index: &index}) + if err != nil { + return err + } + + lrp := lookupLRPInSlice(lrps, actualLRPInstanceKey) + if lrp != nil && lrp.Presence == models.ActualLRP_Suspect { + logger.Info("ignored-claim-request-from-suspect", lager.Data{ + "process_guid": processGUID, + "index": index, + "instance_guid": actualLRPInstanceKey, + "state": lrp.State, + }) + return nil + } + + before, after, err := h.db.ClaimActualLRP(ctx, logger, processGUID, index, actualLRPInstanceKey) + if err != nil { + return err + } + + newLRPs := eventCalculator.RecordChange(before, after, lrps) + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), lrps, newLRPs) + + return nil +} + +func (h *ActualLRPLifecycleController) StartActualLRP(ctx context.Context, + logger lager.Logger, + actualLRPKey *models.ActualLRPKey, + actualLRPInstanceKey *models.ActualLRPInstanceKey, + actualLRPNetInfo *models.ActualLRPNetInfo, + actualLRPInternalRoutes []*models.ActualLRPInternalRoute, + actualLRPMetricTags map[string]string, + routable bool, + availabilityZone string, +) error { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + lrps, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: actualLRPKey.ProcessGuid, Index: &actualLRPKey.Index}) + if err != nil && err != models.ErrResourceNotFound { + return err + } + + lrp := lookupLRPInSlice(lrps, actualLRPInstanceKey) + if lrp != nil && lrp.Presence == models.ActualLRP_Suspect { + logger.Info("ignored-start-request-from-suspect", lager.Data{ + "process_guid": actualLRPKey.ProcessGuid, + "index": actualLRPKey.Index, + "instance_guid": actualLRPInstanceKey, + "state": lrp.State, + }) + return nil + } + + isCurrentlyRunning := lrp != nil && lrp.State == models.ActualLRPStateRunning + + // creates ordinary running actual LRP if it doesn't exist, otherwise updates + // the existing ordinary actual LRP to running state + before, after, err := h.db.StartActualLRP(ctx, logger, actualLRPKey, actualLRPInstanceKey, actualLRPNetInfo, actualLRPInternalRoutes, actualLRPMetricTags, routable, availabilityZone, isCurrentlyRunning) + if err != nil { + return err + } + newLRPs := eventCalculator.RecordChange(before, after, lrps) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), lrps, newLRPs) + }() + + evacuating := findWithPresence(lrps, models.ActualLRP_Evacuating) + suspect := findWithPresence(lrps, models.ActualLRP_Suspect) + + if evacuating != nil { + err = h.evacuationDB.RemoveEvacuatingActualLRP(ctx, logger, &evacuating.ActualLRPKey, &evacuating.ActualLRPInstanceKey) + if err != nil { + logger.Error("failed-to-remove-evacuating-actual-lrp", err, lager.Data{"instance-guid": evacuating.ActualLRPInstanceKey}) + } + newLRPs = eventCalculator.RecordChange(evacuating, nil, newLRPs) + } + + var suspectLRP *models.ActualLRP + // prior to starting this ActualLRP there was a suspect LRP that we need to remove + if suspect != nil { + suspectLRP, err = h.suspectDB.RemoveSuspectActualLRP(ctx, logger, actualLRPKey) + if err != nil { + logger.Error("failed-to-remove-suspect-lrp", err) + } else { + newLRPs = eventCalculator.RecordChange(suspectLRP, nil, newLRPs) + } + } + + return nil +} + +func (h *ActualLRPLifecycleController) CrashActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey, errorMessage string) error { + lrps, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: actualLRPKey.ProcessGuid, Index: &actualLRPKey.Index}) + if err != nil { + return err + } + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + lrp := lookupLRPInSlice(lrps, actualLRPInstanceKey) + traceId := trace.RequestIdFromContext(ctx) + if lrp != nil && lrp.Presence == models.ActualLRP_Suspect { + suspectLRP, err := h.suspectDB.RemoveSuspectActualLRP(ctx, logger, actualLRPKey) + if err != nil { + return err + } + + afterLRPs := eventCalculator.RecordChange(suspectLRP, nil, lrps) + logger.Info("removing-suspect-lrp", lager.Data{"ig": suspectLRP.InstanceGuid}) + go eventCalculator.EmitEvents(traceId, lrps, afterLRPs) + + return nil + } + + before, after, shouldRestart, err := h.db.CrashActualLRP(ctx, logger, actualLRPKey, actualLRPInstanceKey, errorMessage) + if err != nil { + return err + } + + afterLRPs := eventCalculator.RecordChange(before, after, lrps) + go eventCalculator.EmitCrashEvents(traceId, lrps, afterLRPs) + + if !shouldRestart { + return nil + } + + schedInfo, err := h.desiredLRPDB.DesiredLRPSchedulingInfoByProcessGuid(ctx, logger, actualLRPKey.ProcessGuid) + if err != nil { + logger.Error("failed-fetching-desired-lrp", err) + return err + } + + startRequest := auctioneer.NewLRPStartRequestFromSchedulingInfo(schedInfo, int(actualLRPKey.Index)) + logger.Info("start-lrp-auction-request", lager.Data{"app_guid": schedInfo.ProcessGuid, "index": int(actualLRPKey.Index)}) + err = h.auctioneerClient.RequestLRPAuctions(logger, trace.RequestIdFromContext(ctx), []*auctioneer.LRPStartRequest{&startRequest}) + logger.Info("finished-lrp-auction-request", lager.Data{"app_guid": schedInfo.ProcessGuid, "index": int(actualLRPKey.Index)}) + if err != nil { + logger.Error("failed-requesting-auction", err) + } + return nil +} + +func (h *ActualLRPLifecycleController) FailActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, errorMessage string) error { + lrps, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: key.ProcessGuid, Index: &key.Index}) + if err != nil { + return err + } + + before, after, err := h.db.FailActualLRP(ctx, logger, key, errorMessage) + if err != nil && err != models.ErrResourceNotFound { + return err + } + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + newLRPs := eventCalculator.RecordChange(before, after, lrps) + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), lrps, newLRPs) + + return nil +} + +func (h *ActualLRPLifecycleController) RemoveActualLRP(ctx context.Context, logger lager.Logger, processGUID string, index int32, instanceKey *models.ActualLRPInstanceKey) error { + beforeLRPs, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: processGUID, Index: &index}) + if err != nil { + return err + } + + lrp := findWithPresence(beforeLRPs, models.ActualLRP_Ordinary) + if lrp == nil { + return models.ErrResourceNotFound + } + + err = h.db.RemoveActualLRP(ctx, logger, processGUID, index, instanceKey) + if err != nil { + return err + } + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + newLRPs := eventCalculator.RecordChange(lrp, nil, beforeLRPs) + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), beforeLRPs, newLRPs) + + return nil +} + +func (h *ActualLRPLifecycleController) RetireActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey) error { + var err error + var cell *models.CellPresence + + logger = logger.Session("retire-actual-lrp", lager.Data{"process_guid": key.ProcessGuid, "index": key.Index}) + + lrps, err := h.db.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: key.ProcessGuid, Index: &key.Index}) + if err != nil { + return err + } + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + lrp := findWithPresence(lrps, models.ActualLRP_Ordinary) + if lrp == nil { + return models.ErrResourceNotFound + } + + newLRPs := make([]*models.ActualLRP, len(lrps)) + copy(newLRPs, lrps) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), lrps, newLRPs) + }() + + recordChange := func() { + newLRPs = eventCalculator.RecordChange(lrp, nil, lrps) + } + + removeLRP := func() error { + err = h.db.RemoveActualLRP(ctx, logger, lrp.ProcessGuid, lrp.Index, &lrp.ActualLRPInstanceKey) + if err == nil { + recordChange() + } + return err + } + + for retryCount := 0; retryCount < models.RetireActualLRPRetryAttempts; retryCount++ { + switch lrp.State { + case models.ActualLRPStateUnclaimed, models.ActualLRPStateCrashed: + err = removeLRP() + case models.ActualLRPStateClaimed, models.ActualLRPStateRunning: + cell, err = h.serviceClient.CellById(logger, lrp.CellId) + if err != nil { + bbsErr := models.ConvertError(err) + if bbsErr.Type == models.Error_ResourceNotFound { + return removeLRP() + } + return err + } + + var client rep.Client + recordChange() + client, err = h.repClientFactory.CreateClient(cell.RepAddress, cell.RepUrl, trace.RequestIdFromContext(ctx)) + if err != nil { + return err + } + err = client.StopLRPInstance(logger, lrp.ActualLRPKey, lrp.ActualLRPInstanceKey) + } + + if err == nil { + return nil + } + + logger.Error("retrying-failed-retire-of-actual-lrp", err, lager.Data{"attempt": retryCount + 1}) + } + + return err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/evacuation_controller.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/evacuation_controller.go new file mode 100644 index 0000000000..e5ad890676 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/evacuation_controller.go @@ -0,0 +1,527 @@ +package controllers + +import ( + "context" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/events/calculator" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/lager/v3" +) + +type EvacuationController struct { + db db.EvacuationDB + actualLRPDB db.ActualLRPDB + suspectLRPDB db.SuspectDB + desiredLRPDB db.DesiredLRPDB + auctioneerClient auctioneer.Client + actualHub events.Hub + actualLRPInstanceHub events.Hub +} + +func NewEvacuationController( + db db.EvacuationDB, + actualLRPDB db.ActualLRPDB, + suspectLRPDB db.SuspectDB, + desiredLRPDB db.DesiredLRPDB, + auctioneerClient auctioneer.Client, + actualHub events.Hub, + actualLRPInstanceHub events.Hub, +) *EvacuationController { + return &EvacuationController{ + db: db, + actualLRPDB: actualLRPDB, + suspectLRPDB: suspectLRPDB, + desiredLRPDB: desiredLRPDB, + auctioneerClient: auctioneerClient, + actualHub: actualHub, + actualLRPInstanceHub: actualLRPInstanceHub, + } +} + +func (h *EvacuationController) RemoveEvacuatingActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey) error { + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: actualLRPKey.ProcessGuid, Index: &actualLRPKey.Index}) + if err != nil { + return err + } + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + newLRPs := make([]*models.ActualLRP, len(actualLRPs)) + copy(newLRPs, actualLRPs) + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), actualLRPs, newLRPs) + }() + + lrp := lookupLRPInSlice(actualLRPs, actualLRPInstanceKey) + if lrp == nil { + logger.Debug("actual-lrp-not-found", lager.Data{"guid": actualLRPKey.ProcessGuid, "index": actualLRPKey.Index}) + return models.ErrResourceNotFound + } + + if lrp.Presence != models.ActualLRP_Evacuating { + logger.Info("evacuating-lrp-is-empty") + return models.ErrResourceNotFound + } + + evacuatingLRPLogData := lager.Data{ + "process-guid": actualLRPKey.ProcessGuid, + "index": actualLRPKey.Index, + "instance-key": actualLRPInstanceKey, + } + + instance := findWithPresence(actualLRPs, models.ActualLRP_Ordinary) + if instance != nil { + evacuatingLRPLogData["replacement-lrp-instance-key"] = instance.ActualLRPInstanceKey + evacuatingLRPLogData["replacement-state"] = instance.State + evacuatingLRPLogData["replacement-lrp-placement-error"] = instance.PlacementError + } + + logger.Info("removing-stranded-evacuating-actual-lrp", evacuatingLRPLogData) + + err = h.db.RemoveEvacuatingActualLRP(ctx, logger, actualLRPKey, actualLRPInstanceKey) + if err != nil { + return err + } + newLRPs = eventCalculator.RecordChange(lrp, nil, actualLRPs) + + return nil +} + +// removeEvacuatingOrSuspect removes an evacuating or suspect LRP if they +// exist. Returns true if the LRP was found and removed, false otherwise. +// Also returns the new lrp set and any errors encountered. +// +// This is a helper function used by all evacuating controller endpoints +// (e.g. EvacuateClaimedActualLRP) that delete the LRP because transitioning +// the LRP state wouldn't make sense if the presence is Suspect or Evacuating. +func (h *EvacuationController) removeEvacuatingOrSuspect( + ctx context.Context, + logger lager.Logger, + calculator calculator.ActualLRPEventCalculator, + lrps []*models.ActualLRP, + key *models.ActualLRPKey, + instanceKey *models.ActualLRPInstanceKey, +) (bool, []*models.ActualLRP, error) { + lrp := lookupLRPInSlice(lrps, instanceKey) + if lrp == nil { + logger.Debug("actual-lrp-not-found", lager.Data{"guid": key.ProcessGuid, "index": key.Index}) + return false, lrps, models.ErrResourceNotFound + } + + switch lrp.Presence { + case models.ActualLRP_Evacuating: + err := h.db.RemoveEvacuatingActualLRP(ctx, logger, key, instanceKey) + if err != nil { + logger.Error("failed-removing-evacuating-actual-lrp", err) + return false, lrps, err + } + case models.ActualLRP_Suspect: + _, err := h.suspectLRPDB.RemoveSuspectActualLRP(ctx, logger, key) + if err != nil { + logger.Error("failed-removing-suspect-actual-lrp", err) + return false, lrps, err + } + default: + return false, lrps, nil + } + + lrps = calculator.RecordChange(lrp, nil, lrps) + return true, lrps, nil +} + +func (h *EvacuationController) EvacuateClaimedActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey) (bool, error) { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + guid := actualLRPKey.ProcessGuid + index := actualLRPKey.Index + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: guid, Index: &index}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err, lager.Data{"guid": guid, "index": index}) + return false, err + } + + newLRPs := make([]*models.ActualLRP, len(actualLRPs)) + copy(newLRPs, actualLRPs) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), actualLRPs, newLRPs) + }() + + removed, newLRPs, err := h.removeEvacuatingOrSuspect(ctx, logger, eventCalculator, newLRPs, actualLRPKey, actualLRPInstanceKey) + if err != nil { + return false, err + } + + if removed { + return false, nil + } + + // this is an ordinary LRP + before, after, err := h.actualLRPDB.UnclaimActualLRP(ctx, logger, false, actualLRPKey) + bbsErr := models.ConvertError(err) + if bbsErr != nil { + if bbsErr.Type == models.Error_ResourceNotFound { + return false, nil + } + return true, bbsErr + } + + newLRPs = eventCalculator.RecordChange(before, after, newLRPs) + + h.requestAuction(ctx, logger, actualLRPKey) + + return false, nil +} + +func (h *EvacuationController) EvacuateCrashedActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey, errorMessage string) error { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + guid := actualLRPKey.ProcessGuid + index := actualLRPKey.Index + + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: guid, Index: &index}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err) + return err + } + + newLRPs := make([]*models.ActualLRP, len(actualLRPs)) + copy(newLRPs, actualLRPs) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), actualLRPs, newLRPs) + }() + + removed, newLRPs, err := h.removeEvacuatingOrSuspect(ctx, logger, eventCalculator, newLRPs, actualLRPKey, actualLRPInstanceKey) + if err != nil { + return err + } + + if removed { + return nil + } + + before, after, _, err := h.actualLRPDB.CrashActualLRP(ctx, logger, actualLRPKey, actualLRPInstanceKey, errorMessage) + if err != nil { + logger.Error("failed-to-crash-actual-lrp", err) + return err + } + + newLRPs = eventCalculator.RecordChange(before, after, newLRPs) + + return nil +} + +// EvacuateRunningActualLRP evacuates the LRP with the given lrp keys. This +// function has to handle the following cases: +// +// 1. Create a Evacuating LRP if one doesn't already exist and this isn't the +// Ordinary LRP. +// +// 2. Do the evacuation dance if this is the Ordinary LRP +// +// 3. Remove the evacuating LRP if it is no longer needed (an Ordinary is +// running or the desired LRP was removed) +// +// Refer to +// https://github.com/cloudfoundry/diego-notes/tree/2cbd7451#harmonizing-during-evacuation +// for more details. +func (h *EvacuationController) EvacuateRunningActualLRP( + ctx context.Context, + logger lager.Logger, + actualLRPKey *models.ActualLRPKey, + actualLRPInstanceKey *models.ActualLRPInstanceKey, + netInfo *models.ActualLRPNetInfo, + internalRoutes []*models.ActualLRPInternalRoute, + metricTags map[string]string, + routable bool, + availabilityZone string, +) (bool, error) { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + guid := actualLRPKey.ProcessGuid + index := actualLRPKey.Index + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: guid, Index: &index}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err) + return true, err + } + + if len(actualLRPs) == 0 { + return false, nil + } + + newLRPs := make([]*models.ActualLRP, len(actualLRPs)) + copy(newLRPs, actualLRPs) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), actualLRPs, newLRPs) + }() + + // the ActualLRP whose InstanceGuid, and CellId match the method + // parameters. + targetActualLRP := lookupLRPInSlice(actualLRPs, actualLRPInstanceKey) + + instance := findWithPresence(actualLRPs, models.ActualLRP_Ordinary) + + // `instance == nil' means the DesiredLRP has been removed and + // stopInstancesFrom deleted the Ordinary instance. + desiredLRPIsRemoved := instance == nil + + // the replacement is already running or crashed. Wrapped in a function so + // we can short circuit its evaluation if instance is nil. + replacementLRPIsRunning := func() bool { + return !instance.Equal(targetActualLRP) && + (instance.State == models.ActualLRPStateRunning || + instance.State == models.ActualLRPStateCrashed) + } + + if desiredLRPIsRemoved || replacementLRPIsRunning() { + removedEvacuating, err := h.removeEvacuating(ctx, logger, targetActualLRP) + newLRPs = eventCalculator.RecordChange(removedEvacuating, nil, newLRPs) + keepContainer := err != nil + return keepContainer, err + } + + updateStrategy, desiredInstances, err := h.desiredLRPDB.DesiredLRPUpdateStrategyByProcessGuid(ctx, logger, guid) + if err != nil { + if err == models.ErrResourceNotFound { + // the desired LRP has been removed, we can remove the evacuating LRP and delete the container + removedEvacuating, err := h.removeEvacuating(ctx, logger, targetActualLRP) + newLRPs = eventCalculator.RecordChange(removedEvacuating, nil, newLRPs) + keepContainer := err != nil + return keepContainer, err + } + // database error, tell Rep to keep the container + logger.Error("failed-to-get-the-update-strategy", err) + return true, err + } + + if updateStrategy == models.DesiredLRP_UpdateStrategyRecreate { + // With recreate update strategy we should always tell Rep to delete the container + + if targetActualLRP == nil { + // Rep still has the LRP that is removed, tell Rep to delete the container + return false, nil + } + + if targetActualLRP.Presence == models.ActualLRP_Evacuating { + // there should not be an evacuating LRP for recreate update strategy + removedEvacuating, err := h.removeEvacuating(ctx, logger, targetActualLRP) + newLRPs = eventCalculator.RecordChange(removedEvacuating, nil, newLRPs) + keepContainer := err != nil + return keepContainer, err + } + + if targetActualLRP.Presence == models.ActualLRP_Suspect { + // remove the suspect LRP + _, err := h.suspectLRPDB.RemoveSuspectActualLRP(ctx, logger, actualLRPKey) + if err != nil { + logger.Error("failed-removing-suspect-actual-lrp", err) + return true, err + } + newLRPs = eventCalculator.RecordChange(targetActualLRP, nil, newLRPs) + return false, nil + } + + if (targetActualLRP.State == models.ActualLRPStateRunning) || + (targetActualLRP.State == models.ActualLRPStateClaimed) { + _, after, err := h.actualLRPDB.UnclaimActualLRPIfAllRunning(ctx, logger, false, actualLRPKey, desiredInstances) + if err != nil { + if err == models.ErrActualLRPCannotBeUnclaimed { + // With recreate update strategy we only evacuate 1 instance at a time + // keep the container + return true, nil + } + logger.Error("failed-to-unclaim-actual-lrp", err) + return true, err + } + newLRPs = eventCalculator.RecordChange(targetActualLRP, after, newLRPs) + h.requestAuction(ctx, logger, actualLRPKey) + return false, nil + } + + // delete the container + return false, nil + } + + if targetActualLRP == nil || targetActualLRP.Presence == models.ActualLRP_Evacuating { + // Create a new Evacuating LRP or update an existing one + // The replacement LRP is either already requested or converger will request it based on instance counts. + evacuating := findWithPresence(actualLRPs, models.ActualLRP_Evacuating) + + if evacuating != nil && !evacuating.Equal(targetActualLRP) { + // There is already another evacuating instance. Let the Rep know + // that we don't need this instance anymore. We can't have more + // than one evacuating instance. + logger.Info("already-evacuated-by-different-cell") + return false, nil + } + + // FIXME: there might be a bug when the LRP is originally in the CLAIMED + // state. db.EvacuateActualLRP always create an evacuating LRP in the + // running state regardless. + newLRP, err := h.db.EvacuateActualLRP(ctx, logger, actualLRPKey, actualLRPInstanceKey, netInfo, internalRoutes, metricTags, routable, availabilityZone) + + if err != nil { + logger.Error("failed-evacuating-actual-lrp", err) + } + + if err == models.ErrResourceExists { + // nothing to do, the evacuating LRP already exists in the DB + return true, nil + } + + newLRPs = eventCalculator.RecordChange(nil, newLRP, newLRPs) + return true, err + } + + if (targetActualLRP.State == models.ActualLRPStateRunning) || + (targetActualLRP.State == models.ActualLRPStateClaimed) { + // do the evacuation dance. Change the instance from Running/Ordinary + // -> Running/Evacuating and create a new Unclaimed/Ordinary LRP. + err = h.evacuateInstance(ctx, logger, actualLRPs, targetActualLRP) + return true, err + } + + // for all other states, just delete the container. + return false, nil +} + +func (h *EvacuationController) EvacuateStoppedActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey) error { + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + guid := actualLRPKey.ProcessGuid + index := actualLRPKey.Index + + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger, models.ActualLRPFilter{ProcessGuid: guid, Index: &index}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err) + return err + } + + newLRPs := make([]*models.ActualLRP, len(actualLRPs)) + copy(newLRPs, actualLRPs) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), actualLRPs, newLRPs) + }() + + removed, newLRPs, err := h.removeEvacuatingOrSuspect(ctx, logger, eventCalculator, newLRPs, actualLRPKey, actualLRPInstanceKey) + if err != nil { + return err + } + + if removed { + return nil + } + + err = h.actualLRPDB.RemoveActualLRP(ctx, logger, guid, index, actualLRPInstanceKey) + if err != nil { + logger.Error("failed-to-remove-actual-lrp", err) + return err + } + + lrp := lookupLRPInSlice(actualLRPs, actualLRPInstanceKey) + newLRPs = eventCalculator.RecordChange(lrp, nil, newLRPs) + + return nil +} + +func (h *EvacuationController) requestAuction(ctx context.Context, logger lager.Logger, lrpKey *models.ActualLRPKey) { + schedInfo, err := h.desiredLRPDB.DesiredLRPSchedulingInfoByProcessGuid(ctx, logger, lrpKey.ProcessGuid) + if err != nil { + logger.Error("failed-fetching-desired-lrp-scheduling-info", err) + return + } + + startRequest := auctioneer.NewLRPStartRequestFromSchedulingInfo(schedInfo, int(lrpKey.Index)) + err = h.auctioneerClient.RequestLRPAuctions(logger, trace.RequestIdFromContext(ctx), []*auctioneer.LRPStartRequest{&startRequest}) + if err != nil { + logger.Error("failed-requesting-auction", err) + } +} + +func (h *EvacuationController) evacuateInstance(ctx context.Context, logger lager.Logger, allLRPs []*models.ActualLRP, actualLRP *models.ActualLRP) error { + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + evacuating, err := h.db.EvacuateActualLRP(ctx, logger, &actualLRP.ActualLRPKey, &actualLRP.ActualLRPInstanceKey, &actualLRP.ActualLRPNetInfo, actualLRP.ActualLrpInternalRoutes, actualLRP.MetricTags, actualLRP.GetRoutable(), actualLRP.AvailabilityZone) + if err != nil { + return err + } + + // although EvacuateActualLRP above creates a new database record. We + // would like to record that as a change event instead, since the instance + // guid hasn't changed. This will produce a simpler instance event stream + // with a single changed event and keep the group events backward + // compatible. + newLRPs := eventCalculator.RecordChange(actualLRP, evacuating, allLRPs) + + defer func() { + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), allLRPs, newLRPs) + }() + + if actualLRP.Presence == models.ActualLRP_Suspect { + _, err := h.suspectLRPDB.RemoveSuspectActualLRP(ctx, logger, &actualLRP.ActualLRPKey) + if err != nil { + logger.Error("failed-removing-suspect-actual-lrp", err) + return err + } + + return nil + } + + _, after, err := h.actualLRPDB.UnclaimActualLRP(ctx, logger, false, &actualLRP.ActualLRPKey) + if err != nil { + return err + } + + // although UnclaimActualLRP above updates a database record. We would + // like to record that as a create event instead. This will produce a + // simpler instance event stream and keep the group events backward + // compatible. + newLRPs = eventCalculator.RecordChange(nil, after, newLRPs) + + h.requestAuction(ctx, logger, &actualLRP.ActualLRPKey) + return nil +} + +func (h *EvacuationController) removeEvacuating(ctx context.Context, logger lager.Logger, evacuating *models.ActualLRP) (*models.ActualLRP, error) { + if evacuating == nil { + return nil, nil + } + + err := h.db.RemoveEvacuatingActualLRP(ctx, logger, &evacuating.ActualLRPKey, &evacuating.ActualLRPInstanceKey) + + if err == nil { + return evacuating, nil + } + + if err == models.ErrActualLRPCannotBeRemoved { + return nil, nil + } + + return nil, err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/lrp_convergence_controller.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/lrp_convergence_controller.go new file mode 100644 index 0000000000..aab36ca0a3 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/lrp_convergence_controller.go @@ -0,0 +1,355 @@ +package controllers + +import ( + "context" + "sync" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/metrics" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/rep" + "code.cloudfoundry.org/workpool" +) + +//go:generate counterfeiter -generate + +//counterfeiter:generate -o fakes/fake_retirer.go . Retirer +type Retirer interface { + RetireActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey) error +} + +type LRPConvergenceController struct { + logger lager.Logger + clock clock.Clock + lrpDB db.LRPDB + suspectDB db.SuspectDB + domainDB db.DomainDB + actualHub events.Hub + actualLRPInstanceHub events.Hub + auctioneerClient auctioneer.Client + serviceClient serviceclient.ServiceClient + repClientFactory rep.ClientFactory + retirer Retirer + convergenceWorkersSize int + lrpStatMetronNotifier metrics.LRPStatMetronNotifier +} + +func NewLRPConvergenceController( + logger lager.Logger, + clock clock.Clock, + db db.LRPDB, + suspectDB db.SuspectDB, + domainDB db.DomainDB, + actualHub events.Hub, + actualLRPInstanceHub events.Hub, + auctioneerClient auctioneer.Client, + serviceClient serviceclient.ServiceClient, + repClientFactory rep.ClientFactory, + retirer Retirer, + convergenceWorkersSize int, + lrpStatMetronNotifier metrics.LRPStatMetronNotifier, +) *LRPConvergenceController { + return &LRPConvergenceController{ + logger: logger, + clock: clock, + lrpDB: db, + suspectDB: suspectDB, + domainDB: domainDB, + actualHub: actualHub, + actualLRPInstanceHub: actualLRPInstanceHub, + auctioneerClient: auctioneerClient, + serviceClient: serviceClient, + repClientFactory: repClientFactory, + retirer: retirer, + convergenceWorkersSize: convergenceWorkersSize, + lrpStatMetronNotifier: lrpStatMetronNotifier, + } +} + +func (h *LRPConvergenceController) ConvergeLRPs(ctx context.Context) { + logger := h.logger.Session("converge-lrps") + traceId := trace.RequestIdFromContext(ctx) + + start := h.clock.Now() + + var err error + var cellSet models.CellSet + logger.Debug("listing-cells") + + cellSet, err = h.serviceClient.Cells(logger) + if err == models.ErrResourceNotFound { + logger.Info("no-cells-found") + cellSet = models.CellSet{} + } else if err != nil { + logger.Error("failed-listing-cells", err) + // convergence should run again later + return + } + logger.Debug("succeeded-listing-cells") + + convergenceResult := h.lrpDB.ConvergeLRPs(ctx, logger, cellSet) + + events := convergenceResult.Events + for _, e := range events { + go h.actualHub.Emit(e) + } + + instanceEvents := convergenceResult.InstanceEvents + for _, e := range instanceEvents { + go h.actualLRPInstanceHub.Emit(e) + } + + keysToRetire := convergenceResult.KeysToRetire + retireLogger := logger.WithData(lager.Data{"retiring_lrp_count": len(keysToRetire)}) + works := []func(){} + for _, key := range keysToRetire { + dereferencedKey := *key + works = append(works, func() { + err := h.retirer.RetireActualLRP(ctx, retireLogger, &dereferencedKey) + if err != nil { + logger.Error("retiring-lrp-failed", err) + } + }) + } + + startRequests := []*auctioneer.LRPStartRequest{} + startRequestLock := &sync.Mutex{} + + defer func() { + startLogger := logger.WithData(lager.Data{"start_requests_count": len(startRequests)}) + if len(startRequests) > 0 { + startLogger.Debug("requesting-start-auctions") + err = h.auctioneerClient.RequestLRPAuctions(logger, traceId, startRequests) + if err != nil { + startLogger.Error("failed-to-request-starts", err, lager.Data{"lrp_start_auctions": startRequests}) + } + startLogger.Debug("done-requesting-start-auctions") + } + }() + + defer func() { + h.lrpStatMetronNotifier.RecordConvergenceDuration(h.clock.Since(start)) + + domains, err := h.domainDB.FreshDomains(ctx, logger) + if err != nil { + logger.Error("failed-getting-fresh-domains", err) + } + h.lrpStatMetronNotifier.RecordFreshDomains(domains) + + claimed, unclaimed, running, crashed, crashingDesired := h.lrpDB.CountActualLRPsByState(ctx, logger) + desired := h.lrpDB.CountDesiredInstances(ctx, logger) + + h.lrpStatMetronNotifier.RecordLRPCounts( + unclaimed, claimed, running, crashed, + len(convergenceResult.MissingLRPKeys), len(convergenceResult.KeysToRetire), + len(convergenceResult.SuspectRunningKeys), len(convergenceResult.SuspectClaimedKeys), + desired, crashingDesired, + ) + + h.lrpStatMetronNotifier.RecordCellCounts(len(cellSet), len(convergenceResult.MissingCellIds)) + }() + + for _, key := range convergenceResult.MissingLRPKeys { + dereferencedKey := *key + works = append(works, func() { + lrp, err := h.lrpDB.CreateUnclaimedActualLRP(ctx, logger, dereferencedKey.Key) + if err != nil { + logger.Error("failed-to-create-unclaimed-lrp", err, lager.Data{"key": dereferencedKey.Key}) + return + } + + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + go h.actualHub.Emit(models.NewActualLRPCreatedEvent(lrp.ToActualLRPGroup())) + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceCreatedEvent(lrp, traceId)) + + startRequest := auctioneer.NewLRPStartRequestFromSchedulingInfo(dereferencedKey.SchedulingInfo, int(dereferencedKey.Key.Index)) + startRequestLock.Lock() + startRequests = append(startRequests, &startRequest) + startRequestLock.Unlock() + }) + } + + for _, lrpKey := range convergenceResult.UnstartedLRPKeys { + dereferencedKey := *lrpKey + works = append(works, func() { + before, after, err := h.lrpDB.UnclaimActualLRP(ctx, logger, true, dereferencedKey.Key) + if err != nil && err != models.ErrActualLRPCannotBeUnclaimed { + logger.Error("cannot-unclaim-lrp", err, lager.Data{"key": dereferencedKey}) + return + } else if !after.Equal(before) { + logger.Info("emitting-changed-event", lager.Data{"before": before, "after": after}) + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + go h.actualHub.Emit(models.NewActualLRPChangedEvent(before.ToActualLRPGroup(), after.ToActualLRPGroup())) + go func() { + h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceCreatedEvent(after, traceId)) + h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceRemovedEvent(before, traceId)) + }() + } + + startRequest := auctioneer.NewLRPStartRequestFromSchedulingInfo(dereferencedKey.SchedulingInfo, int(dereferencedKey.Key.Index)) + startRequestLock.Lock() + startRequests = append(startRequests, &startRequest) + startRequestLock.Unlock() + }) + } + + suspectKeyMap := map[models.ActualLRPKey]int{} + for _, suspectKey := range convergenceResult.SuspectRunningKeys { + suspectKeyMap[*suspectKey] = 0 + } + for _, suspectKey := range convergenceResult.SuspectClaimedKeys { + suspectKeyMap[*suspectKey] = 0 + } + + for _, key := range convergenceResult.KeysWithMissingCells { + dereferencedKey := *key + handleLRP := func() { + logger := logger.Session("keys-with-missing-cells") + + _, existingSuspect := suspectKeyMap[*dereferencedKey.Key] + if existingSuspect { + // there is a Suspect LRP already, unclaim this previously created + // replacement and reauction it + logger.Debug("found-suspect-lrp-unclaiming", lager.Data{"key": dereferencedKey.Key}) + before, after, err := h.lrpDB.UnclaimActualLRP(ctx, logger, false, dereferencedKey.Key) + if err != nil { + logger.Error("failed-unclaiming-lrp", err) + return + } + + //emit instance events for removing suspect and creating unclaimed + go func() { + h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceCreatedEvent(after, traceId)) + h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceRemovedEvent(before, traceId)) + }() + + return + } + + before, after, err := h.lrpDB.ChangeActualLRPPresence(ctx, logger, dereferencedKey.Key, models.ActualLRP_Ordinary, models.ActualLRP_Suspect) + if err != nil { + logger.Error("cannot-change-lrp-presence", err, lager.Data{"key": dereferencedKey}) + return + } + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceChangedEvent(before, after, traceId)) + + unclaimed, err := h.lrpDB.CreateUnclaimedActualLRP(ctx, logger.Session("create-unclaimed-actual"), dereferencedKey.Key) + if err != nil { + logger.Error("cannot-unclaim-lrp", err) + return + } + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceCreatedEvent(unclaimed, traceId)) + + startRequest := auctioneer.NewLRPStartRequestFromSchedulingInfo(dereferencedKey.SchedulingInfo, int(dereferencedKey.Key.Index)) + startRequestLock.Lock() + startRequests = append(startRequests, &startRequest) + startRequestLock.Unlock() + logger.Info("creating-start-request", + lager.Data{"reason": "missing-cell", "process_guid": dereferencedKey.Key.ProcessGuid, "index": dereferencedKey.Key.Index}) + } + + works = append(works, handleLRP) + } + + for _, key := range convergenceResult.SuspectKeysWithExistingCells { + dereferencedKey := *key + works = append(works, func() { + logger := logger.Session("suspect-keys-with-existing-cells") + beforeLRP, afterLRP, removedLRP, err := h.suspectDB.PromoteSuspectActualLRP(ctx, logger, dereferencedKey.ProcessGuid, dereferencedKey.Index) + if err != nil { + logger.Error("cannot-promote-suspect-lrp", err, lager.Data{"key": dereferencedKey}) + return + } + if removedLRP != nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + go h.actualHub.Emit(models.NewActualLRPRemovedEvent(removedLRP.ToActualLRPGroup())) + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceRemovedEvent(removedLRP, traceId)) + + } + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceChangedEvent(beforeLRP, afterLRP, traceId)) + }) + } + + for _, key := range convergenceResult.SuspectLRPKeysToRetire { + dereferencedKey := *key + works = append(works, func() { + logger := logger.Session("suspect-keys-to-retire") + suspectLRP, err := h.suspectDB.RemoveSuspectActualLRP(ctx, logger, &dereferencedKey) + if err != nil { + logger.Error("cannot-remove-suspect-lrp", err, lager.Data{"key": dereferencedKey}) + return + } + + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + go h.actualHub.Emit(models.NewActualLRPRemovedEvent(suspectLRP.ToActualLRPGroup())) + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceRemovedEvent(suspectLRP, traceId)) + }) + } + + for _, lrpKey := range convergenceResult.KeysWithInternalRouteChanges { + dereferencedLRPKey := *lrpKey + works = append(works, func() { + cellPresence, err := h.serviceClient.CellById(logger, dereferencedLRPKey.InstanceKey.CellId) + if err != nil { + logger.Error("failed-fetching-cell-presence", err) + return + } + + repClient, err := h.repClientFactory.CreateClient(cellPresence.RepAddress, cellPresence.RepUrl, "") + if err != nil { + logger.Error("create-rep-client-failed", err) + return + } + + var internalRoutes models.InternalRoutes + for _, ir := range dereferencedLRPKey.DesiredInternalRoutes { + internalRoutes = append(internalRoutes, models.InternalRoute{Hostname: ir.Hostname}) + } + lrpUpdate := rep.NewLRPUpdate(dereferencedLRPKey.InstanceKey.InstanceGuid, *dereferencedLRPKey.Key, internalRoutes, nil) + err = repClient.UpdateLRPInstance(logger, lrpUpdate) + if err != nil { + logger.Error("updating-lrp-instance", err) + } + }) + } + + for _, lrpKey := range convergenceResult.KeysWithMetricTagChanges { + dereferencedLRPKey := *lrpKey + works = append(works, func() { + cellPresence, err := h.serviceClient.CellById(logger, dereferencedLRPKey.InstanceKey.CellId) + if err != nil { + logger.Error("failed-fetching-cell-presence", err) + return + } + + repClient, err := h.repClientFactory.CreateClient(cellPresence.RepAddress, cellPresence.RepUrl, "") + if err != nil { + logger.Error("create-rep-client-failed", err) + return + } + + lrpUpdate := rep.NewLRPUpdate(dereferencedLRPKey.InstanceKey.InstanceGuid, *dereferencedLRPKey.Key, nil, lrpKey.DesiredMetricTags) + err = repClient.UpdateLRPInstance(logger, lrpUpdate) + if err != nil { + logger.Error("updating-lrp-instance", err) + } + }) + } + + var throttler *workpool.Throttler + throttler, err = workpool.NewThrottler(h.convergenceWorkersSize, works) + if err != nil { + logger.Error("failed-constructing-throttler", err, lager.Data{"max_workers": h.convergenceWorkersSize, "num_works": len(works)}) + return + } + + retireLogger.Debug("retiring-actual-lrps") + throttler.Work() + retireLogger.Debug("done-retiring-actual-lrps") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/package.go new file mode 100644 index 0000000000..615644a35d --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/package.go @@ -0,0 +1 @@ +package controllers // import "code.cloudfoundry.org/bbs/controllers" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/task_controller.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/task_controller.go new file mode 100644 index 0000000000..f2b5328a6a --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/controllers/task_controller.go @@ -0,0 +1,312 @@ +package controllers + +import ( + "context" + "time" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/metrics" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/taskworkpool" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/rep" +) + +type TaskController struct { + db db.TaskDB + taskCompletionClient taskworkpool.TaskCompletionClient + auctioneerClient auctioneer.Client + serviceClient serviceclient.ServiceClient + repClientFactory rep.ClientFactory + taskHub events.Hub + taskStatMetronNotifier metrics.TaskStatMetronNotifier + maxRetries int +} + +func NewTaskController( + db db.TaskDB, + taskCompletionClient taskworkpool.TaskCompletionClient, + auctioneerClient auctioneer.Client, + serviceClient serviceclient.ServiceClient, + repClientFactory rep.ClientFactory, + taskHub events.Hub, + taskStatMetronNotifier metrics.TaskStatMetronNotifier, + maxRetries int, +) *TaskController { + return &TaskController{ + db: db, + taskCompletionClient: taskCompletionClient, + auctioneerClient: auctioneerClient, + serviceClient: serviceClient, + repClientFactory: repClientFactory, + taskHub: taskHub, + taskStatMetronNotifier: taskStatMetronNotifier, + maxRetries: maxRetries, + } +} + +func (c *TaskController) Tasks(ctx context.Context, logger lager.Logger, domain, cellID string) ([]*models.Task, error) { + logger = logger.Session("tasks") + + filter := models.TaskFilter{Domain: domain, CellID: cellID} + return c.db.Tasks(ctx, logger, filter) +} + +func (c *TaskController) TaskByGuid(ctx context.Context, logger lager.Logger, taskGUID string) (*models.Task, error) { + logger = logger.Session("task-by-guid") + + return c.db.TaskByGuid(ctx, logger, taskGUID) +} + +func (c *TaskController) DesireTask(ctx context.Context, logger lager.Logger, taskDefinition *models.TaskDefinition, taskGUID, domain string) error { + var err error + var task *models.Task + logger = logger.Session("desire-task") + + logger = logger.WithData(lager.Data{"task_guid": taskGUID}) + + task, err = c.db.DesireTask(ctx, logger, taskDefinition, taskGUID, domain) + if err != nil { + return err + } + go c.taskHub.Emit(models.NewTaskCreatedEvent(task)) + + logger.Debug("start-task-auction-request") + taskStartRequest := auctioneer.NewTaskStartRequestFromModel(taskGUID, domain, taskDefinition) + err = c.auctioneerClient.RequestTaskAuctions(logger, trace.RequestIdFromContext(ctx), []*auctioneer.TaskStartRequest{&taskStartRequest}) + if err != nil { + logger.Error("failed-requesting-task-auction", err) + // The creation succeeded, the auction request error can be dropped + } else { + logger.Debug("succeeded-requesting-task-auction") + } + + return nil +} + +func (c *TaskController) StartTask(ctx context.Context, logger lager.Logger, taskGUID, cellID string) (shouldStart bool, err error) { + logger = logger.Session("start-task", lager.Data{"task_guid": taskGUID, "cell_id": cellID}) + before, after, shouldStart, err := c.db.StartTask(ctx, logger, taskGUID, cellID) + if err == nil && shouldStart { + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + c.taskStatMetronNotifier.RecordTaskStarted(cellID) + } + return shouldStart, err +} + +func (c *TaskController) CancelTask(ctx context.Context, logger lager.Logger, taskGUID string) error { + logger = logger.Session("cancel-task") + + before, after, cellID, err := c.db.CancelTask(ctx, logger, taskGUID) + if err != nil { + return err + } + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + if after.CompletionCallbackUrl != "" { + logger.Info("task-client-completing-task") + go c.taskCompletionClient.Submit(c.db, c.taskHub, after) + } + + if cellID == "" { + return nil + } + + logger.Info("start-check-cell-presence", lager.Data{"cell_id": cellID}) + cellPresence, err := c.serviceClient.CellById(logger, cellID) + if err != nil { + logger.Error("failed-fetching-cell-presence", err) + // don't return an error, the rep will converge later + return nil + } + logger.Info("finished-check-cell-presence", lager.Data{"cell_id": cellID}) + + repClient, err := c.repClientFactory.CreateClient(cellPresence.RepAddress, cellPresence.RepUrl, trace.RequestIdFromContext(ctx)) + if err != nil { + logger.Error("create-rep-client-failed", err) + return err + } + logger.Info("start-rep-cancel-task", lager.Data{"task_guid": taskGUID}) + err = repClient.CancelTask(logger, taskGUID) + if err != nil { + logger.Error("failed-rep-cancel-task", err) + // don't return an error, the rep will converge later + return nil + } + logger.Info("finished-rep-cancel-task", lager.Data{"task_guid": taskGUID}) + return nil +} + +func (c *TaskController) FailTask(ctx context.Context, logger lager.Logger, taskGUID, failureReason string) error { + var err error + + before, after, err := c.db.FailTask(ctx, logger, taskGUID, failureReason) + if err != nil { + return err + } + + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + if after.CompletionCallbackUrl != "" { + logger.Info("task-client-completing-task") + go c.taskCompletionClient.Submit(c.db, c.taskHub, after) + } + + return nil +} + +func (c *TaskController) RejectTask(ctx context.Context, logger lager.Logger, taskGUID, rejectionReason string) error { + logger = logger.Session("reject-task", lager.Data{"guid": taskGUID}) + logger.Info("start") + defer logger.Info("complete") + + task, err := c.db.TaskByGuid(ctx, logger, taskGUID) + if err != nil { + logger.Error("failed-to-fetch-task", err) + return err + } + + logger.Info("reject-task", lager.Data{"rejection-reason": rejectionReason}) + before, after, rejectTaskErr := c.db.RejectTask(ctx, logger, taskGUID, rejectionReason) + if rejectTaskErr != nil { + logger.Error("failed-to-reject-task", rejectTaskErr) + } + + if int(task.RejectionCount) >= c.maxRetries { + return c.FailTask(ctx, logger, taskGUID, rejectionReason) + } + + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + return rejectTaskErr +} + +func (c *TaskController) CompleteTask( + ctx context.Context, + logger lager.Logger, + taskGUID, + cellID string, + failed bool, + failureReason, + result string, +) error { + var err error + logger = logger.Session("complete-task") + + before, after, err := c.db.CompleteTask(ctx, logger, taskGUID, cellID, failed, failureReason, result) + if err != nil { + return err + } + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + if failed { + c.taskStatMetronNotifier.RecordTaskFailed(cellID) + } else { + c.taskStatMetronNotifier.RecordTaskSucceeded(cellID) + } + + if after.CompletionCallbackUrl != "" { + logger.Info("task-client-completing-task") + go c.taskCompletionClient.Submit(c.db, c.taskHub, after) + } + + return nil +} + +func (c *TaskController) ResolvingTask(ctx context.Context, logger lager.Logger, taskGUID string) error { + logger = logger.Session("resolving-task") + + before, after, err := c.db.ResolvingTask(ctx, logger, taskGUID) + if err != nil { + return err + } + go c.taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + return nil +} + +func (c *TaskController) DeleteTask(ctx context.Context, logger lager.Logger, taskGUID string) error { + logger = logger.Session("delete-task") + + task, err := c.db.DeleteTask(ctx, logger, taskGUID) + if err != nil { + return err + } + go c.taskHub.Emit(models.NewTaskRemovedEvent(task)) + + return nil +} + +func (c *TaskController) ConvergeTasks( + ctx context.Context, + logger lager.Logger, + kickTaskDuration, + expirePendingTaskDuration, + expireCompletedTaskDuration time.Duration, +) error { + var err error + logger = logger.Session("converge-tasks") + + logger.Debug("listing-cells") + cellSet, err := c.serviceClient.Cells(logger) + if err == models.ErrResourceNotFound { + logger.Debug("no-cells-found") + cellSet = models.CellSet{} + } else if err != nil { + logger.Debug("failed-listing-cells") + return err + } + logger.Debug("succeeded-listing-cells") + + convergenceStartTime := time.Now() + taskConvergenceResult := c.db.ConvergeTasks( + ctx, + logger, + cellSet, + kickTaskDuration, + expirePendingTaskDuration, + expireCompletedTaskDuration, + ) + + c.taskStatMetronNotifier.RecordTaskCounts( + taskConvergenceResult.Metrics.TasksPending, + taskConvergenceResult.Metrics.TasksRunning, + taskConvergenceResult.Metrics.TasksCompleted, + taskConvergenceResult.Metrics.TasksResolving, + taskConvergenceResult.Metrics.TasksPruned, + taskConvergenceResult.Metrics.TasksKicked, + ) + + c.taskStatMetronNotifier.RecordConvergenceDuration(time.Since(convergenceStartTime)) + + logger.Debug("emitting-events-from-convergence", lager.Data{"num_tasks_to_complete": len(taskConvergenceResult.TasksToComplete)}) + for _, event := range taskConvergenceResult.Events { + go c.taskHub.Emit(event) + } + + if len(taskConvergenceResult.TasksToAuction) > 0 { + logger.Debug("requesting-task-auctions", lager.Data{"num_tasks_to_auction": len(taskConvergenceResult.TasksToAuction)}) + err = c.auctioneerClient.RequestTaskAuctions(logger, trace.RequestIdFromContext(ctx), taskConvergenceResult.TasksToAuction) + if err != nil { + taskGuids := make([]string, len(taskConvergenceResult.TasksToAuction)) + for i, task := range taskConvergenceResult.TasksToAuction { + taskGuids[i] = task.Task.TaskGuid + } + logger.Error("failed-to-request-auctions-for-pending-tasks", err, + lager.Data{"task_guids": taskGuids}) + } + logger.Debug("done-requesting-task-auctions", lager.Data{"num_tasks_to_auction": len(taskConvergenceResult.TasksToAuction)}) + } + + logger.Debug("submitting-tasks-to-be-completed", lager.Data{"num_tasks_to_complete": len(taskConvergenceResult.TasksToComplete)}) + for _, task := range taskConvergenceResult.TasksToComplete { + c.taskCompletionClient.Submit(c.db, c.taskHub, task) + } + logger.Debug("done-submitting-tasks-to-be-completed", lager.Data{"num_tasks_to_complete": len(taskConvergenceResult.TasksToComplete)}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/converger.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/converger.go new file mode 100644 index 0000000000..843b342e58 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/converger.go @@ -0,0 +1,155 @@ +package converger + +import ( + "context" + "os" + "sync" + "time" + + "code.cloudfoundry.org/lager/v3" + uuid "github.com/nu7hatch/gouuid" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/clock" +) + +//go:generate counterfeiter -generate + +//counterfeiter:generate -o fake_controllers/fake_lrp_convergence_controller.go . LrpConvergenceController +type LrpConvergenceController interface { + ConvergeLRPs(ctx context.Context) +} + +//counterfeiter:generate -o fake_controllers/fake_task_controller.go . TaskController +type TaskController interface { + ConvergeTasks(ctx context.Context, logger lager.Logger, kickTaskDuration, expirePendingTaskDuration, expireCompletedTaskDuration time.Duration) error +} + +type Converger struct { + id string + serviceClient serviceclient.ServiceClient + lrpConvergenceController LrpConvergenceController + taskController TaskController + logger lager.Logger + clock clock.Clock + convergeRepeatInterval time.Duration + kickTaskDuration time.Duration + expirePendingTaskDuration time.Duration + expireCompletedTaskDuration time.Duration + closeOnce *sync.Once +} + +func New( + logger lager.Logger, + clock clock.Clock, + lrpConvergenceController LrpConvergenceController, + taskController TaskController, + serviceClient serviceclient.ServiceClient, + convergeRepeatInterval, + kickTaskDuration, + expirePendingTaskDuration, + expireCompletedTaskDuration time.Duration, +) *Converger { + + uuid, err := uuid.NewV4() + if err != nil { + panic("Failed to generate a random guid....:" + err.Error()) + } + + return &Converger{ + id: uuid.String(), + logger: logger, + clock: clock, + serviceClient: serviceClient, + lrpConvergenceController: lrpConvergenceController, + taskController: taskController, + convergeRepeatInterval: convergeRepeatInterval, + kickTaskDuration: kickTaskDuration, + expirePendingTaskDuration: expirePendingTaskDuration, + expireCompletedTaskDuration: expireCompletedTaskDuration, + closeOnce: &sync.Once{}, + } +} + +func (c *Converger) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := c.logger.Session("converger-process") + logger.Info("started") + + convergeTimer := c.clock.NewTimer(c.convergeRepeatInterval) + defer func() { + logger.Info("done") + convergeTimer.Stop() + }() + + cellEvents := c.serviceClient.CellEvents(logger) + convergeChan := make(chan struct{}, 2) + + close(ready) + + for { + select { + case <-signals: + return nil + + case event := <-cellEvents: + // Stopping the timer in order to avoid a race condition in the tests. + // Executing Stop() removes the timer from the list of watchers on the clock + // which allows us to use WaitForWatcherAndIncrement on the fake clock. + convergeTimer.Stop() + switch event.EventType() { + case models.EventTypeCellDisappeared: + logger.Info("received-cell-disappeared-event", lager.Data{"cell-id": event.CellIDs()}) + c.converge(convergeChan) + } + + case <-convergeTimer.C(): + convergeTimer.Stop() + c.converge(convergeChan) + } + + select { + case <-signals: + return nil + case <-convergeChan: + } + select { + case <-signals: + return nil + case <-convergeChan: + } + + convergeTimer.Reset(c.convergeRepeatInterval) + } +} + +func (c *Converger) converge(convergeChan chan struct{}) { + logger := c.logger.Session("executing-convergence") + + go func() { + logger.Info("converge-tasks-started") + defer logger.Info("converge-tasks-done") + + err := c.taskController.ConvergeTasks( + context.Background(), + c.logger, + c.kickTaskDuration, + c.expirePendingTaskDuration, + c.expireCompletedTaskDuration, + ) + if err != nil { + logger.Error("failed-to-converge-tasks", err) + } + + convergeChan <- struct{}{} + }() + + go func() { + logger.Info("converge-lrps-started") + defer logger.Info("converge-lrps-done") + + c.lrpConvergenceController.ConvergeLRPs(context.Background()) + + convergeChan <- struct{}{} + }() +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/package.go new file mode 100644 index 0000000000..d99e8b021f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/converger/package.go @@ -0,0 +1 @@ +package converger // import "code.cloudfoundry.org/bbs/converger" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/actual_lrp_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/actual_lrp_db.go new file mode 100644 index 0000000000..c26e5a9739 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/actual_lrp_db.go @@ -0,0 +1,39 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . ActualLRPDB + +type ActualLRPDB interface { + ActualLRPs(ctx context.Context, logger lager.Logger, filter models.ActualLRPFilter) ([]*models.ActualLRP, error) + ActualLRPsByProcessGuids(ctx context.Context, logger lager.Logger, filter models.ActualLRPsByProcessGuidsFilter) ([]*models.ActualLRP, error) + CreateUnclaimedActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey) (after *models.ActualLRP, err error) + UnclaimActualLRP(ctx context.Context, logger lager.Logger, isStale bool, key *models.ActualLRPKey) (before *models.ActualLRP, after *models.ActualLRP, err error) + UnclaimActualLRPIfAllRunning(ctx context.Context, logger lager.Logger, isStale bool, key *models.ActualLRPKey, desiredInstances int32) (before *models.ActualLRP, after *models.ActualLRP, err error) + + ClaimActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, instanceKey *models.ActualLRPInstanceKey) (before *models.ActualLRP, after *models.ActualLRP, err error) + StartActualLRP(ctx context.Context, + logger lager.Logger, + key *models.ActualLRPKey, + instanceKey *models.ActualLRPInstanceKey, + netInfo *models.ActualLRPNetInfo, + internalRoutes []*models.ActualLRPInternalRoute, + metricTags map[string]string, + routable bool, + availabilityZone string, + isCurrentlyRunning bool, + ) (before *models.ActualLRP, after *models.ActualLRP, err error) + CrashActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, instanceKey *models.ActualLRPInstanceKey, crashReason string) (before *models.ActualLRP, after *models.ActualLRP, shouldRestart bool, err error) + FailActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, placementError string) (before *models.ActualLRP, after *models.ActualLRP, err error) + RemoveActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, instanceKey *models.ActualLRPInstanceKey) error + + ChangeActualLRPPresence(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, from, to models.ActualLRP_Presence) (before *models.ActualLRP, after *models.ActualLRP, err error) + + CountActualLRPsByState(ctx context.Context, logger lager.Logger) (int, int, int, int, int) + CountDesiredInstances(ctx context.Context, logger lager.Logger) int +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/bbs_health_check.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/bbs_health_check.go new file mode 100644 index 0000000000..8d8dd60e6c --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/bbs_health_check.go @@ -0,0 +1,14 @@ +package db + +import ( + "context" + "time" + + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . BBSHealthCheckDB + +type BBSHealthCheckDB interface { + PerformBBSHealthCheck(ctx context.Context, logger lager.Logger, t time.Time) error +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/db.go new file mode 100644 index 0000000000..a7dee0cfb3 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/db.go @@ -0,0 +1,16 @@ +package db + +//go:generate counterfeiter -generate + +//counterfeiter:generate . DB + +type DB interface { + DomainDB + EncryptionDB + EvacuationDB + LRPDB + TaskDB + VersionDB + SuspectDB + BBSHealthCheckDB +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/desired_lrp_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/desired_lrp_db.go new file mode 100644 index 0000000000..da2a883197 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/desired_lrp_db.go @@ -0,0 +1,25 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . DesiredLRPDB + +type DesiredLRPDB interface { + DesiredLRPs(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRP, error) + DesiredLRPByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (*models.DesiredLRP, error) + + DesiredLRPSchedulingInfos(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRPSchedulingInfo, error) + DesiredLRPSchedulingInfoByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (*models.DesiredLRPSchedulingInfo, error) + DesiredLRPUpdateStrategyByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (models.DesiredLRP_UpdateStrategy, int32, error) + + DesiredLRPRoutingInfos(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRP, error) + + DesireLRP(ctx context.Context, logger lager.Logger, desiredLRP *models.DesiredLRP) error + UpdateDesiredLRP(ctx context.Context, logger lager.Logger, processGuid string, update *models.DesiredLRPUpdate) (beforeDesiredLRP *models.DesiredLRP, err error) + RemoveDesiredLRP(ctx context.Context, logger lager.Logger, processGuid string) error +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/domain_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/domain_db.go new file mode 100644 index 0000000000..c5bd5ec012 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/domain_db.go @@ -0,0 +1,13 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . DomainDB +type DomainDB interface { + FreshDomains(ctx context.Context, logger lager.Logger) ([]string, error) + UpsertDomain(ctx context.Context, lgger lager.Logger, domain string, ttl uint32) error +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/encryption_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/encryption_db.go new file mode 100644 index 0000000000..fcf03ac19d --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/encryption_db.go @@ -0,0 +1,15 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . EncryptionDB + +type EncryptionDB interface { + EncryptionKeyLabel(ctx context.Context, logger lager.Logger) (string, error) + SetEncryptionKeyLabel(ctx context.Context, logger lager.Logger, encryptionKeyLabel string) error + PerformEncryption(ctx context.Context, logger lager.Logger) error +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/evacuation_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/evacuation_db.go new file mode 100644 index 0000000000..f95c9fe908 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/evacuation_db.go @@ -0,0 +1,15 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . EvacuationDB + +type EvacuationDB interface { + RemoveEvacuatingActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey) error + EvacuateActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey, *models.ActualLRPNetInfo, []*models.ActualLRPInternalRoute, map[string]string, bool, string) (actualLRP *models.ActualLRP, err error) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/lrp_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/lrp_db.go new file mode 100644 index 0000000000..ed07e97caf --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/lrp_db.go @@ -0,0 +1,46 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/routing-info/internalroutes" +) + +type ActualLRPKeyWithInternalRoutes struct { + Key *models.ActualLRPKey + InstanceKey *models.ActualLRPInstanceKey + DesiredInternalRoutes internalroutes.InternalRoutes +} + +type ActualLRPKeyWithMetricTags struct { + Key *models.ActualLRPKey + InstanceKey *models.ActualLRPInstanceKey + DesiredMetricTags map[string]string +} + +//counterfeiter:generate . LRPDB + +type ConvergenceResult struct { + MissingLRPKeys []*models.ActualLRPKeyWithSchedulingInfo + UnstartedLRPKeys []*models.ActualLRPKeyWithSchedulingInfo + SuspectKeysWithExistingCells []*models.ActualLRPKey + SuspectLRPKeysToRetire []*models.ActualLRPKey + SuspectRunningKeys []*models.ActualLRPKey + SuspectClaimedKeys []*models.ActualLRPKey + KeysToRetire []*models.ActualLRPKey + KeysWithMissingCells []*models.ActualLRPKeyWithSchedulingInfo + KeysWithInternalRouteChanges []*ActualLRPKeyWithInternalRoutes + KeysWithMetricTagChanges []*ActualLRPKeyWithMetricTags + MissingCellIds []string + Events []models.Event + InstanceEvents []models.Event +} + +type LRPDB interface { + ActualLRPDB + DesiredLRPDB + + ConvergeLRPs(ctx context.Context, logger lager.Logger, cellSet models.CellSet) ConvergenceResult +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1461790966_init_sql.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1461790966_init_sql.go new file mode 100644 index 0000000000..84f0fbaf50 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1461790966_init_sql.go @@ -0,0 +1,200 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewInitSQL()) +} + +type InitSQL struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewInitSQL() migration.Migration { + return &InitSQL{} +} + +func (e *InitSQL) String() string { + return migrationString(e) +} + +func (e *InitSQL) Version() int64 { + return 1461790966 +} + +func (e *InitSQL) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *InitSQL) SetClock(c clock.Clock) { e.clock = c } +func (e *InitSQL) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *InitSQL) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("init-sql") + logger.Info("truncating-tables") + + // Ignore the error as the tables may not exist + _ = e.dropTables(tx) + + err := e.createTables(tx, logger) + if err != nil { + return err + } + + err = e.createIndices(tx, logger) + if err != nil { + return err + } + + return nil +} + +func (e *InitSQL) dropTables(tx *sql.Tx) error { + tableNames := []string{ + "domains", + "tasks", + "desired_lrps", + "actual_lrps", + } + for _, tableName := range tableNames { + _, err := tx.Exec("DROP TABLE IF EXISTS " + tableName) + if err != nil { + return err + } + } + return nil +} + +func (e *InitSQL) createTables(tx *sql.Tx, logger lager.Logger) error { + var createTablesSQL = []string{ + helpers.RebindForFlavor(createDomainSQL, e.dbFlavor), + helpers.RebindForFlavor(createDesiredLRPsSQL, e.dbFlavor), + helpers.RebindForFlavor(createActualLRPsSQL, e.dbFlavor), + helpers.RebindForFlavor(createTasksSQL, e.dbFlavor), + } + + logger.Info("creating-tables") + for _, query := range createTablesSQL { + logger.Info("creating the table", lager.Data{"query": query}) + _, err := tx.Exec(query) + if err != nil { + logger.Error("failed-creating-tables", err) + return err + } + logger.Info("created the table", lager.Data{"query": query}) + } + + return nil +} + +func (e *InitSQL) createIndices(tx *sql.Tx, logger lager.Logger) error { + logger.Info("creating-indices") + createIndicesSQL := []string{} + createIndicesSQL = append(createIndicesSQL, createDomainsIndices...) + createIndicesSQL = append(createIndicesSQL, createDesiredLRPsIndices...) + createIndicesSQL = append(createIndicesSQL, createActualLRPsIndices...) + createIndicesSQL = append(createIndicesSQL, createTasksIndices...) + + for _, query := range createIndicesSQL { + logger.Info("creating the index", lager.Data{"query": query}) + _, err := tx.Exec(query) + if err != nil { + logger.Error("failed-creating-index", err) + return err + } + logger.Info("created the index", lager.Data{"query": query}) + } + + return nil +} + +const createDomainSQL = `CREATE TABLE domains( + domain VARCHAR(255) PRIMARY KEY, + expire_time BIGINT DEFAULT 0 +);` + +const createDesiredLRPsSQL = `CREATE TABLE desired_lrps( + process_guid VARCHAR(255) PRIMARY KEY, + domain VARCHAR(255) NOT NULL, + log_guid VARCHAR(255) NOT NULL, + annotation MEDIUMTEXT, + instances INT NOT NULL, + memory_mb INT NOT NULL, + disk_mb INT NOT NULL, + rootfs VARCHAR(255) NOT NULL, + routes MEDIUMTEXT NOT NULL, + volume_placement MEDIUMTEXT NOT NULL, + modification_tag_epoch VARCHAR(255) NOT NULL, + modification_tag_index INT, + run_info MEDIUMTEXT NOT NULL +);` + +const createActualLRPsSQL = `CREATE TABLE actual_lrps( + process_guid VARCHAR(255), + instance_index INT, + evacuating BOOL DEFAULT false, + domain VARCHAR(255) NOT NULL, + state VARCHAR(255) NOT NULL, + instance_guid VARCHAR(255) NOT NULL DEFAULT '', + cell_id VARCHAR(255) NOT NULL DEFAULT '', + placement_error VARCHAR(255) NOT NULL DEFAULT '', + since BIGINT DEFAULT 0, + net_info MEDIUMTEXT NOT NULL, + modification_tag_epoch VARCHAR(255) NOT NULL, + modification_tag_index INT, + crash_count INT NOT NULL DEFAULT 0, + crash_reason VARCHAR(255) NOT NULL DEFAULT '', + expire_time BIGINT DEFAULT 0, + + PRIMARY KEY(process_guid, instance_index, evacuating) +);` + +const createTasksSQL = `CREATE TABLE tasks( + guid VARCHAR(255) PRIMARY KEY, + domain VARCHAR(255) NOT NULL, + updated_at BIGINT DEFAULT 0, + created_at BIGINT DEFAULT 0, + first_completed_at BIGINT DEFAULT 0, + state INT, + cell_id VARCHAR(255) NOT NULL DEFAULT '', + result MEDIUMTEXT, + failed BOOL DEFAULT false, + failure_reason VARCHAR(255) NOT NULL DEFAULT '', + task_definition MEDIUMTEXT NOT NULL +);` + +var createDomainsIndices = []string{ + `CREATE INDEX domains_expire_time_idx ON domains (expire_time)`, +} + +var createDesiredLRPsIndices = []string{ + `CREATE INDEX desired_lrps_domain_idx ON desired_lrps (domain)`, +} + +var createActualLRPsIndices = []string{ + `CREATE INDEX actual_lrps_domain_idx ON actual_lrps (domain)`, + `CREATE INDEX actual_lrps_cell_id_idx ON actual_lrps (cell_id)`, + `CREATE INDEX actual_lrps_since_idx ON actual_lrps (since)`, + `CREATE INDEX actual_lrps_state_idx ON actual_lrps (state)`, + `CREATE INDEX actual_lrps_expire_time_idx ON actual_lrps (expire_time)`, +} + +var createTasksIndices = []string{ + `CREATE INDEX tasks_domain_idx ON tasks (domain)`, + `CREATE INDEX tasks_state_idx ON tasks (state)`, + `CREATE INDEX tasks_cell_id_idx ON tasks (cell_id)`, + `CREATE INDEX tasks_updated_at_idx ON tasks (updated_at)`, + `CREATE INDEX tasks_created_at_idx ON tasks (created_at)`, + `CREATE INDEX tasks_first_completed_at_idx ON tasks (first_completed_at)`, +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1471030898_increase_run_info_column.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1471030898_increase_run_info_column.go new file mode 100644 index 0000000000..debd9e51c3 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1471030898_increase_run_info_column.go @@ -0,0 +1,86 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewIncreaseRunInfoColumnSize()) +} + +type IncreaseRunInfoColumnSize struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewIncreaseRunInfoColumnSize() migration.Migration { + return &IncreaseRunInfoColumnSize{} +} + +func (e *IncreaseRunInfoColumnSize) String() string { + return migrationString(e) +} + +func (e *IncreaseRunInfoColumnSize) Version() int64 { + return 1471030898 +} + +func (e *IncreaseRunInfoColumnSize) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *IncreaseRunInfoColumnSize) SetClock(c clock.Clock) { e.clock = c } +func (e *IncreaseRunInfoColumnSize) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *IncreaseRunInfoColumnSize) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("increase-run-info-column") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTables(tx, logger) +} + +func (e *IncreaseRunInfoColumnSize) alterTables(tx *sql.Tx, logger lager.Logger) error { + if e.dbFlavor != "mysql" { + return nil + } + + var alterTablesSQL = []string{ + alterDesiredLRPsSQL, + alterActualLRPsSQL, + alterTasksSQL, + } + + logger.Info("altering-tables") + for _, query := range alterTablesSQL { + logger.Info("altering the table", lager.Data{"query": query}) + _, err := tx.Exec(query) + if err != nil { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": query}) + } + + return nil +} + +const alterDesiredLRPsSQL = `ALTER TABLE desired_lrps + MODIFY annotation MEDIUMTEXT, + MODIFY routes MEDIUMTEXT NOT NULL, + MODIFY volume_placement MEDIUMTEXT NOT NULL, + MODIFY run_info MEDIUMTEXT NOT NULL;` + +const alterActualLRPsSQL = `ALTER TABLE actual_lrps + MODIFY net_info MEDIUMTEXT NOT NULL;` + +const alterTasksSQL = `ALTER TABLE tasks + MODIFY result MEDIUMTEXT, + MODIFY task_definition MEDIUMTEXT NOT NULL;` diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1472757022_add_placement_tags_to_desired_lrp.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1472757022_add_placement_tags_to_desired_lrp.go new file mode 100644 index 0000000000..fda869d91f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1472757022_add_placement_tags_to_desired_lrp.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddPlacementTagsToDesiredLRPs()) +} + +type AddPlacementTagsToDesiredLRPs struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddPlacementTagsToDesiredLRPs() migration.Migration { + return &AddPlacementTagsToDesiredLRPs{} +} + +func (e *AddPlacementTagsToDesiredLRPs) String() string { + return migrationString(e) +} + +func (e *AddPlacementTagsToDesiredLRPs) Version() int64 { + return 1472757022 +} + +func (e *AddPlacementTagsToDesiredLRPs) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddPlacementTagsToDesiredLRPs) SetClock(c clock.Clock) { e.clock = c } +func (e *AddPlacementTagsToDesiredLRPs) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddPlacementTagsToDesiredLRPs) Up(tx *sql.Tx, logger lager.Logger) error { + var alterDesiredLRPAddPlacementTagSQL string + if e.dbFlavor == "mysql" { + alterDesiredLRPAddPlacementTagSQL = `ALTER TABLE desired_lrps + ADD COLUMN placement_tags TEXT;` + } else { + alterDesiredLRPAddPlacementTagSQL = `ALTER TABLE desired_lrps + ADD COLUMN IF NOT EXISTS placement_tags TEXT;` + } + logger.Info("altering the table", lager.Data{"query": alterDesiredLRPAddPlacementTagSQL}) + _, err := tx.Exec(alterDesiredLRPAddPlacementTagSQL) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterDesiredLRPAddPlacementTagSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474908092_increase_error_columns.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474908092_increase_error_columns.go new file mode 100644 index 0000000000..b8f6ee7d78 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474908092_increase_error_columns.go @@ -0,0 +1,74 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewIncreaseErrorColumnsSize()) +} + +type IncreaseErrorColumnsSize struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewIncreaseErrorColumnsSize() migration.Migration { + return &IncreaseErrorColumnsSize{} +} + +func (e *IncreaseErrorColumnsSize) String() string { + return migrationString(e) +} + +func (e *IncreaseErrorColumnsSize) Version() int64 { + return 1474908092 +} + +func (e *IncreaseErrorColumnsSize) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *IncreaseErrorColumnsSize) SetClock(c clock.Clock) { e.clock = c } +func (e *IncreaseErrorColumnsSize) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *IncreaseErrorColumnsSize) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("increase-run-info-column") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTables(tx, logger) +} + +func (e *IncreaseErrorColumnsSize) alterTables(tx *sql.Tx, logger lager.Logger) error { + var alterActualLRPsSQL string + + if e.dbFlavor == "mysql" { + alterActualLRPsSQL = `ALTER TABLE actual_lrps + MODIFY crash_reason VARCHAR(1024) NOT NULL DEFAULT '', + MODIFY placement_error VARCHAR(1024) NOT NULL DEFAULT ''` + + } else { + alterActualLRPsSQL = `ALTER TABLE actual_lrps + ALTER crash_reason TYPE VARCHAR(1024), + ALTER placement_error TYPE VARCHAR(1024)` + } + + logger.Info("altering-tables") + logger.Info("altering the table", lager.Data{"query": alterActualLRPsSQL}) + _, err := tx.Exec(alterActualLRPsSQL) + if err != nil { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterActualLRPsSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474993971_encrypt_routes.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474993971_encrypt_routes.go new file mode 100644 index 0000000000..f2725e2e9a --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1474993971_encrypt_routes.go @@ -0,0 +1,99 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewEncryptRoutes()) +} + +type EncryptRoutes struct { + encoder format.Encoder + clock clock.Clock + dbFlavor string +} + +func NewEncryptRoutes() migration.Migration { + return &EncryptRoutes{} +} + +func (e *EncryptRoutes) String() string { + return migrationString(e) +} + +func (e *EncryptRoutes) Version() int64 { + return 1474993971 +} + +func (e *EncryptRoutes) SetCryptor(cryptor encryption.Cryptor) { + e.encoder = format.NewEncoder(cryptor) +} + +func (e *EncryptRoutes) SetClock(c clock.Clock) { e.clock = c } +func (e *EncryptRoutes) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *EncryptRoutes) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("encrypt-route-column") + logger.Info("starting") + defer logger.Info("completed") + + query := "SELECT process_guid, routes FROM desired_lrps" + + rows, err := tx.Query(query) + if err != nil { + logger.Error("failed-query", err) + return err + } + + routeDataMap := map[string][]byte{} + + var processGuid string + var routeData []byte + + if rows.Err() != nil { + logger.Error("failed-fetching-row", rows.Err()) + return rows.Err() + } + + for rows.Next() { + err := rows.Scan(&processGuid, &routeData) + if err != nil { + logger.Error("failed-reading-row", err) + continue + } + routeDataMap[processGuid] = routeData + } + err = rows.Close() + if err != nil { + logger.Error("failed-to-close-row", err) + } + + for pGuid, rData := range routeDataMap { + encodedData, err := e.encoder.Encode(rData) + if err != nil { + logger.Error("failed-encrypting-routes", err) + return models.ErrBadRequest + } + + bindings := make([]interface{}, 0, 3) + updateQuery := "UPDATE desired_lrps SET routes = ? WHERE process_guid = ?" + bindings = append(bindings, encodedData) + bindings = append(bindings, pGuid) + _, err = tx.Exec(helpers.RebindForFlavor(updateQuery, e.dbFlavor), bindings...) + if err != nil { + logger.Error("failed-updating-desired-lrp-record", err) + return models.ErrBadRequest + } + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1481761088_add_max_pid_limit.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1481761088_add_max_pid_limit.go new file mode 100644 index 0000000000..be14ae9bfd --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1481761088_add_max_pid_limit.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddMaxPidsToDesiredLRPs()) +} + +type AddMaxPidsToDesiredLRPs struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddMaxPidsToDesiredLRPs() migration.Migration { + return &AddMaxPidsToDesiredLRPs{} +} + +func (e *AddMaxPidsToDesiredLRPs) String() string { + return migrationString(e) +} + +func (e *AddMaxPidsToDesiredLRPs) Version() int64 { + return 1481761088 +} + +func (e *AddMaxPidsToDesiredLRPs) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddMaxPidsToDesiredLRPs) SetClock(c clock.Clock) { e.clock = c } +func (e *AddMaxPidsToDesiredLRPs) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddMaxPidsToDesiredLRPs) Up(tx *sql.Tx, logger lager.Logger) error { + var alterDesiredLRPAddMaxPidsSQL string + if e.dbFlavor == "mysql" { + alterDesiredLRPAddMaxPidsSQL = `ALTER TABLE desired_lrps + ADD COLUMN max_pids INTEGER DEFAULT 0;` + } else { + alterDesiredLRPAddMaxPidsSQL = `ALTER TABLE desired_lrps + ADD COLUMN IF NOT EXISTS max_pids INTEGER DEFAULT 0;` + } + logger.Info("altering the table", lager.Data{"query": alterDesiredLRPAddMaxPidsSQL}) + _, err := tx.Exec(alterDesiredLRPAddMaxPidsSQL) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterDesiredLRPAddMaxPidsSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1502289152_increase_rootfs_column.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1502289152_increase_rootfs_column.go new file mode 100644 index 0000000000..1425696ed4 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1502289152_increase_rootfs_column.go @@ -0,0 +1,72 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewIncreaseRootFSColumnSize()) +} + +type IncreaseRootFSColumnsSize struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewIncreaseRootFSColumnSize() migration.Migration { + return new(IncreaseRootFSColumnsSize) +} + +func (e *IncreaseRootFSColumnsSize) String() string { + return migrationString(e) +} + +func (e *IncreaseRootFSColumnsSize) Version() int64 { + return 1502289152 +} + +func (e *IncreaseRootFSColumnsSize) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *IncreaseRootFSColumnsSize) SetClock(c clock.Clock) { e.clock = c } +func (e *IncreaseRootFSColumnsSize) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *IncreaseRootFSColumnsSize) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("increase-rootfs-column") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTables(tx, logger) +} + +func (e *IncreaseRootFSColumnsSize) alterTables(tx *sql.Tx, logger lager.Logger) error { + var alterActualLRPsSQL string + + if e.dbFlavor == "mysql" { + alterActualLRPsSQL = `ALTER TABLE desired_lrps + MODIFY rootfs VARCHAR(1024) NOT NULL DEFAULT ''` + + } else { + alterActualLRPsSQL = `ALTER TABLE desired_lrps + ALTER rootfs TYPE VARCHAR(1024)` + } + + logger.Info("altering-tables") + logger.Info("altering the table", lager.Data{"query": alterActualLRPsSQL}) + _, err := tx.Exec(alterActualLRPsSQL) + if err != nil { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterActualLRPsSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1522700600_add_task_rejection_count.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1522700600_add_task_rejection_count.go new file mode 100644 index 0000000000..a9b75e33f3 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1522700600_add_task_rejection_count.go @@ -0,0 +1,59 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddTaskRejectionCount()) +} + +type AddTaskRejectionCount struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddTaskRejectionCount() migration.Migration { + return new(AddTaskRejectionCount) +} + +func (e *AddTaskRejectionCount) String() string { + return migrationString(e) +} + +func (e *AddTaskRejectionCount) Version() int64 { + return 1522700600 +} + +func (e *AddTaskRejectionCount) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddTaskRejectionCount) SetClock(c clock.Clock) { e.clock = c } +func (e *AddTaskRejectionCount) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddTaskRejectionCount) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("add-task-rejection-count") + logger.Info("starting") + defer logger.Info("completed") + + var stmt string + if e.dbFlavor == "mysql" { + stmt = "ALTER TABLE tasks ADD COLUMN rejection_count INTEGER NOT NULL DEFAULT 0;" + } else { + stmt = "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS rejection_count INTEGER NOT NULL DEFAULT 0;" + } + _, err := tx.Exec(stmt) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1523050077_add_rejection_reason_to_task.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1523050077_add_rejection_reason_to_task.go new file mode 100644 index 0000000000..958d0c9423 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1523050077_add_rejection_reason_to_task.go @@ -0,0 +1,59 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddRejectionReasonToTask()) +} + +type AddRejectionReasonToTask struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddRejectionReasonToTask() migration.Migration { + return new(AddRejectionReasonToTask) +} + +func (e *AddRejectionReasonToTask) String() string { + return migrationString(e) +} + +func (e *AddRejectionReasonToTask) Version() int64 { + return 1523050077 +} + +func (e *AddRejectionReasonToTask) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddRejectionReasonToTask) SetClock(c clock.Clock) { e.clock = c } +func (e *AddRejectionReasonToTask) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddRejectionReasonToTask) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("add-task-rejection-reason") + logger.Info("starting") + defer logger.Info("completed") + + var query string + if e.dbFlavor == "mysql" { + query = "ALTER TABLE tasks ADD COLUMN rejection_reason VARCHAR(255) NOT NULL DEFAULT '';" + } else { + query = "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS rejection_reason VARCHAR(255) NOT NULL DEFAULT '';" + } + _, err := tx.Exec(query) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1528928964_increase_task_error_columns.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1528928964_increase_task_error_columns.go new file mode 100644 index 0000000000..53c212eb1c --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1528928964_increase_task_error_columns.go @@ -0,0 +1,74 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewIncreaseTaskErrorColumns()) +} + +type IncreaseTaskErrorColumns struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewIncreaseTaskErrorColumns() migration.Migration { + return new(IncreaseTaskErrorColumns) +} + +func (e *IncreaseTaskErrorColumns) String() string { + return migrationString(e) +} + +func (e *IncreaseTaskErrorColumns) Version() int64 { + return 1528928964 +} + +func (e *IncreaseTaskErrorColumns) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *IncreaseTaskErrorColumns) SetClock(c clock.Clock) { e.clock = c } +func (e *IncreaseTaskErrorColumns) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *IncreaseTaskErrorColumns) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("increase-failure-reason-column") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTables(tx, logger) +} + +func (e *IncreaseTaskErrorColumns) alterTables(tx *sql.Tx, logger lager.Logger) error { + var alterTaskTableSQL string + + if e.dbFlavor == "mysql" { + alterTaskTableSQL = `ALTER TABLE tasks + MODIFY rejection_reason VARCHAR(1024) NOT NULL DEFAULT '', + MODIFY failure_reason VARCHAR(1024) NOT NULL DEFAULT ''` + + } else { + alterTaskTableSQL = `ALTER TABLE tasks + ALTER rejection_reason TYPE VARCHAR(1024), + ALTER failure_reason TYPE VARCHAR(1024)` + } + + logger.Info("altering-tables") + logger.Info("altering the table", lager.Data{"query": alterTaskTableSQL}) + _, err := tx.Exec(alterTaskTableSQL) + if err != nil { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterTaskTableSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1529530809_add_presence_to_actual_lrp.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1529530809_add_presence_to_actual_lrp.go new file mode 100644 index 0000000000..20c09e6b87 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1529530809_add_presence_to_actual_lrp.go @@ -0,0 +1,92 @@ +package migrations + +import ( + "database/sql" + "fmt" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddPresenceToActualLrp()) +} + +type AddPresenceToActualLrp struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddPresenceToActualLrp() migration.Migration { + return new(AddPresenceToActualLrp) +} + +func (e *AddPresenceToActualLrp) String() string { + return migrationString(e) +} + +func (e *AddPresenceToActualLrp) Version() int64 { + return 1529530809 +} + +func (e *AddPresenceToActualLrp) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddPresenceToActualLrp) SetClock(c clock.Clock) { e.clock = c } +func (e *AddPresenceToActualLrp) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddPresenceToActualLrp) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("add-presence") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTable(tx, logger) +} + +func (e *AddPresenceToActualLrp) alterTable(tx *sql.Tx, logger lager.Logger) error { + var addColumnSQL string + if e.dbFlavor == "mysql" { + addColumnSQL = "ALTER TABLE actual_lrps ADD COLUMN presence INT NOT NULL DEFAULT 0;" + } else { + addColumnSQL = "ALTER TABLE actual_lrps ADD COLUMN IF NOT EXISTS presence INT NOT NULL DEFAULT 0;" + } + + logger.Info("altering-table") + _, err := tx.Exec(helpers.RebindForFlavor(addColumnSQL, e.dbFlavor)) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + + alterTablesSQL := []string{} + alterTablesSQL = append(alterTablesSQL, fmt.Sprintf("UPDATE actual_lrps SET presence = %d WHERE evacuating = true;", models.ActualLRP_Evacuating)) + + if e.dbFlavor == "mysql" { + alterTablesSQL = append(alterTablesSQL, + "ALTER TABLE actual_lrps DROP primary key, ADD PRIMARY KEY (process_guid, instance_index, presence);", + ) + } else { + alterTablesSQL = append(alterTablesSQL, + "ALTER TABLE actual_lrps DROP CONSTRAINT actual_lrps_pkey, ADD PRIMARY KEY (process_guid, instance_index, presence);", + ) + } + + for _, query := range alterTablesSQL { + logger.Info("altering the table", lager.Data{"query": query}) + _, err := tx.Exec(helpers.RebindForFlavor(query, e.dbFlavor)) + if err != nil { + logger.Error("failed-altering-table", err) + return err + } + logger.Info("altered the table", lager.Data{"query": query}) + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1643660541_add_internal_routes_to_actual_lrp.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1643660541_add_internal_routes_to_actual_lrp.go new file mode 100644 index 0000000000..edbf307b93 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1643660541_add_internal_routes_to_actual_lrp.go @@ -0,0 +1,63 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddInternalRoutesToActualLrp()) +} + +type AddInternalRoutesToActualLrp struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddInternalRoutesToActualLrp() migration.Migration { + return new(AddInternalRoutesToActualLrp) +} + +func (e *AddInternalRoutesToActualLrp) String() string { + return migrationString(e) +} + +func (e *AddInternalRoutesToActualLrp) Version() int64 { + return 1643660541 +} + +func (e *AddInternalRoutesToActualLrp) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddInternalRoutesToActualLrp) SetClock(c clock.Clock) { e.clock = c } +func (e *AddInternalRoutesToActualLrp) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddInternalRoutesToActualLrp) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("add-internal-routes") + logger.Info("starting") + defer logger.Info("completed") + + var alterTableSQL string + if e.dbFlavor == "mysql" { + alterTableSQL = "ALTER TABLE actual_lrps ADD COLUMN internal_routes MEDIUMTEXT;" + } else { + alterTableSQL = "ALTER TABLE actual_lrps ADD COLUMN IF NOT EXISTS internal_routes MEDIUMTEXT;" + } + logger.Info("altering the table", lager.Data{"query": alterTableSQL}) + _, err := tx.Exec(helpers.RebindForFlavor(alterTableSQL, e.dbFlavor)) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterTableSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1674146125_set_run_info_longtext.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1674146125_set_run_info_longtext.go new file mode 100644 index 0000000000..ae10d0e5fb --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1674146125_set_run_info_longtext.go @@ -0,0 +1,86 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewSetRunInfoLongtext()) +} + +type SetRunInfoLongtext struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewSetRunInfoLongtext() migration.Migration { + return &SetRunInfoLongtext{} +} + +func (e *SetRunInfoLongtext) String() string { + return migrationString(e) +} + +func (e *SetRunInfoLongtext) Version() int64 { + return 1674146125 +} + +func (e *SetRunInfoLongtext) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *SetRunInfoLongtext) SetClock(c clock.Clock) { e.clock = c } +func (e *SetRunInfoLongtext) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *SetRunInfoLongtext) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("set-run-info-longtext") + logger.Info("starting") + defer logger.Info("completed") + + return e.alterTables(tx, logger) +} + +func (e *SetRunInfoLongtext) alterTables(tx *sql.Tx, logger lager.Logger) error { + if e.dbFlavor != "mysql" { + return nil + } + + alterDesiredLRPsSQL := `ALTER TABLE desired_lrps + MODIFY annotation LONGTEXT, + MODIFY routes LONGTEXT NOT NULL, + MODIFY volume_placement LONGTEXT NOT NULL, + MODIFY run_info LONGTEXT NOT NULL;` + + alterActualLRPsSQL := `ALTER TABLE actual_lrps + MODIFY net_info LONGTEXT NOT NULL;` + + alterTasksSQL := `ALTER TABLE tasks + MODIFY result LONGTEXT, + MODIFY task_definition LONGTEXT NOT NULL;` + + var alterTablesSQL = []string{ + alterDesiredLRPsSQL, + alterActualLRPsSQL, + alterTasksSQL, + } + + logger.Info("altering-tables") + for _, query := range alterTablesSQL { + logger.Info("altering the table", lager.Data{"query": query}) + _, err := tx.Exec(query) + if err != nil { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": query}) + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1676360874_add_metric_tags_to_actual_lrp.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1676360874_add_metric_tags_to_actual_lrp.go new file mode 100644 index 0000000000..04e99173ca --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1676360874_add_metric_tags_to_actual_lrp.go @@ -0,0 +1,63 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddMetricTagsToActualLrp()) +} + +type AddMetricTagsToActualLrp struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddMetricTagsToActualLrp() migration.Migration { + return new(AddMetricTagsToActualLrp) +} + +func (e *AddMetricTagsToActualLrp) String() string { + return migrationString(e) +} + +func (e *AddMetricTagsToActualLrp) Version() int64 { + return 1676360874 +} + +func (e *AddMetricTagsToActualLrp) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddMetricTagsToActualLrp) SetClock(c clock.Clock) { e.clock = c } +func (e *AddMetricTagsToActualLrp) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddMetricTagsToActualLrp) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("add-metric-tags") + logger.Info("starting") + defer logger.Info("completed") + + var alterTableSQL string + if e.dbFlavor == "mysql" { + alterTableSQL = "ALTER TABLE actual_lrps ADD COLUMN metric_tags MEDIUMTEXT;" + } else { + alterTableSQL = "ALTER TABLE actual_lrps ADD COLUMN IF NOT EXISTS metric_tags MEDIUMTEXT;" + } + logger.Info("altering the table", lager.Data{"query": alterTableSQL}) + _, err := tx.Exec(helpers.RebindForFlavor(alterTableSQL, e.dbFlavor)) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterTableSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1686692176_add_routable_to_actual_lrps.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1686692176_add_routable_to_actual_lrps.go new file mode 100644 index 0000000000..a19fdeb39c --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1686692176_add_routable_to_actual_lrps.go @@ -0,0 +1,73 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddRoutableToActualLrps()) +} + +type AddRoutableToActualLrps struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddRoutableToActualLrps() migration.Migration { + return new(AddRoutableToActualLrps) +} + +func (e *AddRoutableToActualLrps) String() string { + return migrationString(e) +} + +func (e *AddRoutableToActualLrps) Version() int64 { + return 1686692176 +} + +func (e *AddRoutableToActualLrps) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddRoutableToActualLrps) SetClock(c clock.Clock) { e.clock = c } +func (e *AddRoutableToActualLrps) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddRoutableToActualLrps) Up(tx *sql.Tx, logger lager.Logger) error { + var alterActualLRPAddRoutableSQL string + if e.dbFlavor == "mysql" { + alterActualLRPAddRoutableSQL = `ALTER TABLE actual_lrps +ADD COLUMN routable BOOL DEFAULT false;` + } else { + alterActualLRPAddRoutableSQL = `ALTER TABLE actual_lrps +ADD COLUMN IF NOT EXISTS routable BOOL DEFAULT false;` + } + + logger.Info("altering the table", lager.Data{"query": alterActualLRPAddRoutableSQL}) + _, err := tx.Exec(alterActualLRPAddRoutableSQL) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + + logger.Info("altered the table", lager.Data{"query": alterActualLRPAddRoutableSQL}) + + logger.Info("altering the table", lager.Data{"query": alterActualLRPSetRoutableForRunningSQL}) + _, err = tx.Exec(alterActualLRPSetRoutableForRunningSQL) + if err != nil { + logger.Error("failed-altering-table", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterActualLRPSetRoutableForRunningSQL}) + + return nil +} + +const alterActualLRPSetRoutableForRunningSQL = `UPDATE actual_lrps +SET routable = true WHERE state = 'RUNNING';` diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1698182853_add_availability_zone_to_actual_lrps.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1698182853_add_availability_zone_to_actual_lrps.go new file mode 100644 index 0000000000..967bf64ab6 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1698182853_add_availability_zone_to_actual_lrps.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddAvailabilityZoneToActualLrps()) +} + +type AddAvailabilityZoneToActualLrps struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddAvailabilityZoneToActualLrps() migration.Migration { + return new(AddAvailabilityZoneToActualLrps) +} + +func (e *AddAvailabilityZoneToActualLrps) String() string { + return migrationString(e) +} + +func (e *AddAvailabilityZoneToActualLrps) Version() int64 { + return 1698182853 +} + +func (e *AddAvailabilityZoneToActualLrps) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddAvailabilityZoneToActualLrps) SetClock(c clock.Clock) { e.clock = c } +func (e *AddAvailabilityZoneToActualLrps) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddAvailabilityZoneToActualLrps) Up(tx *sql.Tx, logger lager.Logger) error { + var alterActualLRPAddAvailabilityZoneSQL string + if e.dbFlavor == "mysql" { + alterActualLRPAddAvailabilityZoneSQL = `ALTER TABLE actual_lrps +ADD COLUMN availability_zone VARCHAR(255) NOT NULL DEFAULT '';` + } else { + alterActualLRPAddAvailabilityZoneSQL = `ALTER TABLE actual_lrps +ADD COLUMN IF NOT EXISTS availability_zone VARCHAR(255) NOT NULL DEFAULT '';` + } + logger.Info("altering the table", lager.Data{"query": alterActualLRPAddAvailabilityZoneSQL}) + _, err := tx.Exec(alterActualLRPAddAvailabilityZoneSQL) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterActualLRPAddAvailabilityZoneSQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1722634733_split_metric_tags.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1722634733_split_metric_tags.go new file mode 100644 index 0000000000..78a77c77e7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1722634733_split_metric_tags.go @@ -0,0 +1,133 @@ +package migrations + +import ( + "database/sql" + "encoding/json" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewSplitMetricTags()) +} + +type SplitMetricTags struct { + encoder format.Encoder + serializer format.Serializer + clock clock.Clock + rawSQLDB *sql.DB + dbFlavor string +} + +func NewSplitMetricTags() migration.Migration { + return new(SplitMetricTags) +} + +func (e *SplitMetricTags) String() string { + return migrationString(e) +} + +func (e *SplitMetricTags) Version() int64 { + return 1722634733 +} + +func (e *SplitMetricTags) SetCryptor(cryptor encryption.Cryptor) { + e.encoder = format.NewEncoder(cryptor) + e.serializer = format.NewSerializer(cryptor) +} + +func (e *SplitMetricTags) SetRawSQLDB(db *sql.DB) { e.rawSQLDB = db } +func (e *SplitMetricTags) SetClock(c clock.Clock) { e.clock = c } +func (e *SplitMetricTags) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *SplitMetricTags) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("split-metric-tags-desired-lrps") + logger.Info("starting") + defer logger.Info("completed") + + var addColumnSQL string + if e.dbFlavor == "mysql" { + addColumnSQL = "ALTER TABLE desired_lrps ADD COLUMN metric_tags MEDIUMTEXT" + } else { + addColumnSQL = "ALTER TABLE desired_lrps ADD COLUMN IF NOT EXISTS metric_tags MEDIUMTEXT" + } + + logger.Info("altering-table") + _, err := tx.Exec(helpers.RebindForFlavor(addColumnSQL, e.dbFlavor)) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-table", err) + return err + } + + query := "SELECT process_guid, run_info FROM desired_lrps" + + rows, err := tx.Query(query) + if err != nil { + logger.Error("failed-query", err) + return err + } + + metricTagsMap := map[string]map[string]*models.MetricTagValue{} + + var processGuid string + var runInfoData []byte + + if rows.Err() != nil { + logger.Error("failed-fetching-row", rows.Err()) + return rows.Err() + } + + for rows.Next() { + err := rows.Scan(&processGuid, &runInfoData) + if err != nil { + logger.Error("failed-reading-row", err) + continue + } + var runInfo models.DesiredLRPRunInfo + err = e.serializer.Unmarshal(logger, runInfoData, &runInfo) + if err != nil { + logger.Error("failed-parsing-run-info", err) + continue + } + metricTags := map[string]*models.MetricTagValue{} + for k, v := range runInfo.MetricTags { + metricTags[k] = v + } + metricTagsMap[processGuid] = metricTags + } + err = rows.Close() + if err != nil { + logger.Error("failed-to-close-row", err) + } + + for pGuid, metricTags := range metricTagsMap { + updateQuery := "UPDATE desired_lrps SET metric_tags = ? WHERE process_guid = ?" + + mData, err := json.Marshal(metricTags) + if err != nil { + logger.Error("failed-marshalling-metric-tags", err) + continue + } + + encodedData, err := e.encoder.Encode(mData) + if err != nil { + logger.Error("failed-encoding-metric-tags", err) + continue + } + + bindings := []interface{}{encodedData, pGuid} + _, err = tx.Exec(helpers.RebindForFlavor(updateQuery, e.dbFlavor), bindings...) + if err != nil { + logger.Error("failed-updating-desired-lrp-record", err) + return models.ErrBadRequest + } + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1764949919_health_check.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1764949919_health_check.go new file mode 100644 index 0000000000..5672885deb --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1764949919_health_check.go @@ -0,0 +1,67 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewBBSHealthCheckTable()) +} + +type BBSHealthCheckTable struct { + encoder format.Encoder + serializer format.Serializer + clock clock.Clock + rawSQLDB *sql.DB + dbFlavor string +} + +func NewBBSHealthCheckTable() migration.Migration { + return new(BBSHealthCheckTable) +} + +func (e *BBSHealthCheckTable) String() string { + return migrationString(e) +} + +func (e *BBSHealthCheckTable) Version() int64 { + return 1764949919 +} + +func (e *BBSHealthCheckTable) SetCryptor(cryptor encryption.Cryptor) { + e.encoder = format.NewEncoder(cryptor) + e.serializer = format.NewSerializer(cryptor) +} + +func (e *BBSHealthCheckTable) SetRawSQLDB(db *sql.DB) { e.rawSQLDB = db } +func (e *BBSHealthCheckTable) SetClock(c clock.Clock) { e.clock = c } +func (e *BBSHealthCheckTable) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *BBSHealthCheckTable) Up(tx *sql.Tx, logger lager.Logger) error { + logger = logger.Session("bbs-health-check-table") + logger.Info("starting") + defer logger.Info("completed") + + var addTableSQL string + if e.dbFlavor == "mysql" { + addTableSQL = "CREATE TABLE IF NOT EXISTS bbs_health_check (id int NOT NULL AUTO_INCREMENT, PRIMARY KEY (id), time bigint NOT NULL)" + } else { + addTableSQL = "CREATE TABLE IF NOT EXISTS bbs_health_check (id SERIAL PRIMARY KEY, time bigint NOT NULL)" + } + + logger.Info("creating-table") + _, err := tx.Exec(helpers.RebindForFlavor(addTableSQL, e.dbFlavor)) + if err != nil { + logger.Error("failed-creating-table", err) + return err + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1772581987_add_update_strategy_to_desired_lrp.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1772581987_add_update_strategy_to_desired_lrp.go new file mode 100644 index 0000000000..6bda8769e8 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/1772581987_add_update_strategy_to_desired_lrp.go @@ -0,0 +1,60 @@ +package migrations + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/migration" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +func init() { + appendMigration(NewAddUpdateStrategyToDesiredLRPs()) +} + +type AddUpdateStrategyToDesiredLRPs struct { + serializer format.Serializer + clock clock.Clock + dbFlavor string +} + +func NewAddUpdateStrategyToDesiredLRPs() migration.Migration { + return &AddUpdateStrategyToDesiredLRPs{} +} + +func (e *AddUpdateStrategyToDesiredLRPs) String() string { + return migrationString(e) +} + +func (e *AddUpdateStrategyToDesiredLRPs) Version() int64 { + return 1772581987 +} + +func (e *AddUpdateStrategyToDesiredLRPs) SetCryptor(cryptor encryption.Cryptor) { + e.serializer = format.NewSerializer(cryptor) +} + +func (e *AddUpdateStrategyToDesiredLRPs) SetClock(c clock.Clock) { e.clock = c } +func (e *AddUpdateStrategyToDesiredLRPs) SetDBFlavor(flavor string) { e.dbFlavor = flavor } + +func (e *AddUpdateStrategyToDesiredLRPs) Up(tx *sql.Tx, logger lager.Logger) error { + var alterDesiredLRPAddUpdateStrategySQL string + if e.dbFlavor == "mysql" { + alterDesiredLRPAddUpdateStrategySQL = `ALTER TABLE desired_lrps + ADD COLUMN update_strategy INT NOT NULL DEFAULT 0;` + } else { + alterDesiredLRPAddUpdateStrategySQL = `ALTER TABLE desired_lrps + ADD COLUMN IF NOT EXISTS update_strategy INT NOT NULL DEFAULT 0;` + } + logger.Info("altering the table", lager.Data{"query": alterDesiredLRPAddUpdateStrategySQL}) + _, err := tx.Exec(alterDesiredLRPAddUpdateStrategySQL) + if err != nil && !isDuplicateColumnError(err) { + logger.Error("failed-altering-tables", err) + return err + } + logger.Info("altered the table", lager.Data{"query": alterDesiredLRPAddUpdateStrategySQL}) + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/migrations.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/migrations.go new file mode 100644 index 0000000000..76fa19110b --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/migrations.go @@ -0,0 +1,54 @@ +package migrations + +import ( + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + + "code.cloudfoundry.org/bbs/migration" + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" +) + +var migrationsRegistry = migration.Migrations{} + +func appendMigration(migrationTemplate migration.Migration) { + migrationsRegistry = append(migrationsRegistry, migrationTemplate) +} + +func migrationString(m migration.Migration) string { + _, filename, _, ok := runtime.Caller(1) + if !ok { + return strconv.FormatInt(m.Version(), 10) + } + return strings.Split(filepath.Base(filename), ".")[0] +} + +func AllMigrations() migration.Migrations { + migs := make(migration.Migrations, len(migrationsRegistry)) + for i, mig := range migrationsRegistry { + rt := reflect.TypeOf(mig) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + migs[i] = reflect.New(rt).Interface().(migration.Migration) + } + return migs +} + +func isDuplicateColumnError(err error) bool { + switch e := err.(type) { + case *mysql.MySQLError: + if e.Number == 1060 { + return true + } + case *pgconn.PgError: + if e.Code == "42701" { + return true + } + } + + return false +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/package.go new file mode 100644 index 0000000000..08f8827a54 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/migrations/package.go @@ -0,0 +1 @@ +package migrations // import "code.cloudfoundry.org/bbs/db/migrations" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/package.go new file mode 100644 index 0000000000..7c1e139cad --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/package.go @@ -0,0 +1 @@ +package db // import "code.cloudfoundry.org/bbs/db" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/actual_lrp_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/actual_lrp_db.go new file mode 100644 index 0000000000..bd84bae1d1 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/actual_lrp_db.go @@ -0,0 +1,871 @@ +package sqldb + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const ( + Truncated = "(truncated)" +) + +func (db *SQLDB) getActualLRPs(ctx context.Context, logger lager.Logger, wheres string, whereBindings ...interface{}) ([]*models.ActualLRP, error) { + var actualLRPs []*models.ActualLRP + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + rows, err := db.all(ctx, logger, tx, actualLRPsTable, + actualLRPColumns, helpers.NoLockRow, + wheres, whereBindings..., + ) + if err != nil { + logger.Error("failed-query", err) + return err + } + defer rows.Close() + actualLRPs, err = db.scanAndCleanupActualLRPs(ctx, logger, tx, rows) + return err + }) + + return actualLRPs, err +} + +func (db *SQLDB) ChangeActualLRPPresence(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, from, to models.ActualLRP_Presence) (before *models.ActualLRP, after *models.ActualLRP, err error) { + logger = logger.Session("db-change-actual-lrp-presence", lager.Data{"key": key, "from": from, "to": to}) + logger.Info("starting") + defer logger.Info("finished") + + var beforeLRP *models.ActualLRP + var afterLRP models.ActualLRP + err = db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + beforeLRP, err = db.fetchActualLRPForUpdate(ctx, logger, key.ProcessGuid, key.Index, from, tx) + if err != nil { + logger.Error("failed-fetching-lrp", err) + return err + } + + afterLRP = *beforeLRP + afterLRP.Presence = to + wheres := "process_guid = ? AND instance_index = ? AND presence = ?" + _, err = db.update(ctx, logger, tx, actualLRPsTable, helpers.SQLAttributes{ + "presence": afterLRP.Presence, + }, wheres, key.ProcessGuid, key.Index, beforeLRP.Presence) + if err != nil { + logger.Error("failed-updating-lrp", err) + } + return err + }) + + return beforeLRP, &afterLRP, err +} + +func (db *SQLDB) ActualLRPs(ctx context.Context, logger lager.Logger, filter models.ActualLRPFilter) ([]*models.ActualLRP, error) { + logger = logger.Session("db-actual-lrps", lager.Data{"filter": filter}) + logger.Debug("starting") + defer logger.Debug("complete") + + var wheres []string + var values []interface{} + + if filter.Domain != "" { + wheres = append(wheres, "domain = ?") + values = append(values, filter.Domain) + } + + if filter.CellID != "" { + wheres = append(wheres, "cell_id = ?") + values = append(values, filter.CellID) + } + + if filter.ProcessGuid != "" { + wheres = append(wheres, "process_guid = ?") + values = append(values, filter.ProcessGuid) + } + + if filter.Index != nil { + wheres = append(wheres, "instance_index = ?") + values = append(values, *filter.Index) + } + + lrps, err := db.getActualLRPs(ctx, logger, strings.Join(wheres, " AND "), values...) + if err != nil { + return nil, err + } + + return lrps, nil +} + +func (db *SQLDB) ActualLRPsByProcessGuids(ctx context.Context, logger lager.Logger, filter models.ActualLRPsByProcessGuidsFilter) ([]*models.ActualLRP, error) { + logger = logger.Session("db-multiple-actual-lrps", lager.Data{"filter": filter}) + logger.Debug("starting") + defer logger.Debug("complete") + + var wheres []string + var values []interface{} + + if len(filter.ProcessGuids) > 0 { + placeholders := make([]string, len(filter.ProcessGuids)) + for i, guid := range filter.ProcessGuids { + placeholders[i] = "?" + values = append(values, guid) + } + wheres = append(wheres, fmt.Sprintf("process_guid IN (%s)", strings.Join(placeholders, ","))) + } + + lrps, err := db.getActualLRPs(ctx, logger, strings.Join(wheres, " AND "), values...) + if err != nil { + return nil, err + } + + return lrps, nil +} + +func (db *SQLDB) CreateUnclaimedActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey) (*models.ActualLRP, error) { + logger = logger.Session("db-create-unclaimed-actual-lrps", lager.Data{"key": key}) + logger.Info("starting") + defer logger.Info("complete") + + guid, err := db.guidProvider.NextGUID() + if err != nil { + logger.Error("failed-to-generate-guid", err) + return nil, models.ErrGUIDGeneration + } + + netInfoData, err := db.serializeModel(logger, &models.ActualLRPNetInfo{}) + if err != nil { + logger.Error("failed-to-serialize-net-info", err) + return nil, err + } + + internalRoutesData, err := db.encodeInternalRouteData(logger, []*models.ActualLRPInternalRoute{}) + if err != nil { + logger.Error("failed-to-serialize-internal-routes", err) + return nil, err + } + + metricTagsData, err := db.encodeMetricTagsData(logger, map[string]string{}) + if err != nil { + logger.Error("failed-to-serialize-metric-tags", err) + return nil, err + } + + now := db.clock.Now().UnixNano() + err = db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + _, err := db.insert(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "process_guid": key.ProcessGuid, + "instance_index": key.Index, + "domain": key.Domain, + "state": models.ActualLRPStateUnclaimed, + "since": now, + "net_info": netInfoData, + "modification_tag_epoch": guid, + "modification_tag_index": 0, + "internal_routes": internalRoutesData, + "metric_tags": metricTagsData, + "routable": false, + }, + ) + + return err + }) + if err != nil { + logger.Error("failed-to-create-unclaimed-actual-lrp", err) + return nil, err + } + lrp := &models.ActualLRP{ + ActualLRPKey: *key, + State: models.ActualLRPStateUnclaimed, + Since: now, + ModificationTag: models.ModificationTag{Epoch: guid, Index: 0}, + ActualLrpInternalRoutes: []*models.ActualLRPInternalRoute{}, + MetricTags: map[string]string{}, + } + lrp.SetRoutable(false) + return lrp, nil +} + +func (db *SQLDB) UnclaimActualLRP(ctx context.Context, logger lager.Logger, isStale bool, key *models.ActualLRPKey) (*models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("db-unclaim-actual-lrp", lager.Data{"key": key}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeActualLRP *models.ActualLRP + var actualLRP *models.ActualLRP + processGuid := key.ProcessGuid + index := key.Index + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + beforeActualLRP, actualLRP, err = db.unclaimActualLRP(ctx, logger, tx, processGuid, index, isStale) + return err + }) + + return beforeActualLRP, actualLRP, err +} + +func (db *SQLDB) UnclaimActualLRPIfAllRunning(ctx context.Context, logger lager.Logger, isStale bool, key *models.ActualLRPKey, desiredInstances int32) (*models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("db-unclaim-actual-lrp-if-all-running", lager.Data{"key": key}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeActualLRP *models.ActualLRP + var actualLRP *models.ActualLRP + processGuid := key.ProcessGuid + index := key.Index + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + err := db.lockDesiredLRPByGuidForUpdate(ctx, logger, processGuid, tx) + if err != nil { + logger.Error("failed-locking-desired-lrp-by-guid", err) + return err + } + actualInstances, err := db.countActualLRPsForProcessGuidAndState(ctx, logger, processGuid, models.ActualLRPStateRunning, models.ActualLRP_Ordinary, tx) + if err != nil { + logger.Error("failed-to-count-actual-lrps-for-process-guid-and-state", err) + return err + } + + if actualInstances < int(desiredInstances) { + logger.Debug("not-enough-running-actual-lrps", lager.Data{"actual": actualInstances, "desired": desiredInstances}) + return models.ErrActualLRPCannotBeUnclaimed + } + beforeActualLRP, actualLRP, err = db.unclaimActualLRP(ctx, logger, tx, processGuid, index, isStale) + return err + }) + + return beforeActualLRP, actualLRP, err +} + +func (db *SQLDB) ClaimActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, instanceKey *models.ActualLRPInstanceKey) (*models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("db-claim-actual-lrp", lager.Data{"process_guid": processGuid, "index": index, "instance_key": instanceKey}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeActualLRP models.ActualLRP + var actualLRP *models.ActualLRP + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Ordinary, tx) + if err != nil { + logger.Error("failed-fetching-actual-lrp-for-share", err) + return err + } + beforeActualLRP = *actualLRP + + if !actualLRP.AllowsTransitionTo(&actualLRP.ActualLRPKey, instanceKey, models.ActualLRPStateClaimed) { + logger.Error("cannot-transition-to-claimed", nil, lager.Data{"from_state": actualLRP.State, "same_instance_key": actualLRP.ActualLRPInstanceKey.Equal(instanceKey)}) + return models.ErrActualLRPCannotBeClaimed + } + + if actualLRP.State == models.ActualLRPStateClaimed && actualLRP.ActualLRPInstanceKey.Equal(instanceKey) { + return nil + } + + actualLRP.ModificationTag.Increment() + actualLRP.State = models.ActualLRPStateClaimed + actualLRP.ActualLRPInstanceKey = *instanceKey + actualLRP.PlacementError = "" + actualLRP.ActualLRPNetInfo = models.ActualLRPNetInfo{} + actualLRP.Since = db.clock.Now().UnixNano() + netInfoData, err := db.serializeModel(logger, &models.ActualLRPNetInfo{}) + if err != nil { + logger.Error("failed-to-serialize-net-info", err) + return err + } + + _, err = db.update(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "state": actualLRP.State, + "cell_id": actualLRP.CellId, + "instance_guid": actualLRP.InstanceGuid, + "modification_tag_index": actualLRP.ModificationTag.Index, + "placement_error": actualLRP.PlacementError, + "since": actualLRP.Since, + "net_info": netInfoData, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + processGuid, index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-claiming-actual-lrp", err) + return err + } + + return nil + }) + + return &beforeActualLRP, actualLRP, err +} + +func (db *SQLDB) StartActualLRP( + ctx context.Context, + logger lager.Logger, + key *models.ActualLRPKey, + instanceKey *models.ActualLRPInstanceKey, + netInfo *models.ActualLRPNetInfo, + internalRoutes []*models.ActualLRPInternalRoute, + metricTags map[string]string, + routable bool, + availabilityZone string, + isCurrentlyRunning bool, +) (*models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("db-start-actual-lrp", lager.Data{"actual_lrp_key": key, "actual_lrp_instance_key": instanceKey, "net_info": netInfo, "routable": routable}) + if db.debugStartActualLRPHeartbeats && isCurrentlyRunning { + logger.Debug("heartbeating-lrp") + defer logger.Debug("heartbeat-complete") + } else { + logger.Info("starting") + defer logger.Info("complete") + } + + var beforeActualLRP models.ActualLRP + var actualLRP *models.ActualLRP + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, tx) + if err == models.ErrResourceNotFound { + actualLRP, err = db.createRunningActualLRP(ctx, logger, key, instanceKey, netInfo, internalRoutes, metricTags, routable, availabilityZone, tx) + return err + } + + if err != nil { + logger.Error("failed-to-get-actual-lrp", err) + return err + } + + beforeActualLRP = *actualLRP + + if actualLRP.ActualLRPKey.Equal(key) && + actualLRP.ActualLRPInstanceKey.Equal(instanceKey) && + actualLRP.ActualLRPNetInfo.Equal(netInfo) && + reflect.DeepEqual(actualLRP.ActualLrpInternalRoutes, internalRoutes) && + reflect.DeepEqual(actualLRP.MetricTags, metricTags) && + actualLRP.GetRoutable() == routable && + actualLRP.AvailabilityZone == availabilityZone && + actualLRP.State == models.ActualLRPStateRunning { + logger.Debug("nothing-to-change") + return nil + } + + if !actualLRP.AllowsTransitionTo(key, instanceKey, models.ActualLRPStateRunning) { + logger.Error("failed-to-transition-actual-lrp-to-started", nil) + return models.ErrActualLRPCannotBeStarted + } + + now := db.clock.Now().UnixNano() + + actualLRP.ActualLRPInstanceKey = *instanceKey + actualLRP.ActualLRPNetInfo = *netInfo + actualLRP.ActualLrpInternalRoutes = internalRoutes + actualLRP.MetricTags = metricTags + actualLRP.AvailabilityZone = availabilityZone + actualLRP.State = models.ActualLRPStateRunning + actualLRP.Since = now + actualLRP.ModificationTag.Increment() + actualLRP.PlacementError = "" + actualLRP.SetRoutable(routable) + + netInfoData, err := db.serializeModel(logger, &actualLRP.ActualLRPNetInfo) + if err != nil { + logger.Error("failed-to-serialize-net-info", err) + return err + } + + internalRoutesData, err := db.encodeInternalRouteData(logger, internalRoutes) + if err != nil { + logger.Error("failed-to-serialize-internalroutes", err) + return err + } + + metricTagsData, err := db.encodeMetricTagsData(logger, metricTags) + if err != nil { + logger.Error("failed-to-serialize-metric-tags", err) + return err + } + + _, err = db.update(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "state": actualLRP.State, + "cell_id": actualLRP.CellId, + "instance_guid": actualLRP.InstanceGuid, + "modification_tag_index": actualLRP.ModificationTag.Index, + "placement_error": actualLRP.PlacementError, + "since": actualLRP.Since, + "net_info": netInfoData, + "internal_routes": internalRoutesData, + "metric_tags": metricTagsData, + "routable": actualLRP.GetRoutable(), + "availability_zone": actualLRP.AvailabilityZone, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-starting-actual-lrp", err) + return err + } + + return nil + }) + + return &beforeActualLRP, actualLRP, err +} + +func truncateString(s string, maxLen int) string { + l := len(s) + if l < maxLen { + return s + } + return s[:maxLen-len(Truncated)] + Truncated +} + +func (db *SQLDB) CrashActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, instanceKey *models.ActualLRPInstanceKey, crashReason string) (*models.ActualLRP, *models.ActualLRP, bool, error) { + logger = logger.Session("db-crash-actual-lrp", lager.Data{"key": key, "instance_key": instanceKey, "crash_reason": crashReason}) + logger.Info("starting") + defer logger.Info("complete") + + immediateRestart := false + var beforeActualLRP models.ActualLRP + var actualLRP *models.ActualLRP + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, tx) + if err != nil { + logger.Error("failed-to-get-actual-lrp", err) + return err + } + beforeActualLRP = *actualLRP + + latestChangeTime := time.Duration(db.clock.Now().UnixNano() - actualLRP.Since) + + var newCrashCount int32 + if latestChangeTime > models.CrashResetTimeout && actualLRP.State == models.ActualLRPStateRunning { + newCrashCount = 1 + } else { + newCrashCount = actualLRP.CrashCount + 1 + } + + if !actualLRP.AllowsTransitionTo(&actualLRP.ActualLRPKey, instanceKey, models.ActualLRPStateCrashed) { + logger.Error("failed-to-transition-to-crashed", nil, lager.Data{"from_state": actualLRP.State, "same_instance_key": actualLRP.ActualLRPInstanceKey.Equal(instanceKey)}) + return models.ErrActualLRPCannotBeCrashed + } + + actualLRP.ModificationTag.Increment() + actualLRP.State = models.ActualLRPStateCrashed + + actualLRP.ActualLRPInstanceKey.InstanceGuid = "" + actualLRP.ActualLRPInstanceKey.CellId = "" + actualLRP.ActualLRPNetInfo = models.ActualLRPNetInfo{} + actualLRP.CrashCount = newCrashCount + actualLRP.CrashReason = crashReason + netInfoData, err := db.serializeModel(logger, &actualLRP.ActualLRPNetInfo) + if err != nil { + logger.Error("failed-to-serialize-net-info", err) + return err + } + + if actualLRP.ShouldRestartImmediately(models.NewDefaultRestartCalculator()) { + actualLRP.State = models.ActualLRPStateUnclaimed + immediateRestart = true + } + + now := db.clock.Now().UnixNano() + actualLRP.Since = now + + _, err = db.update(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "state": actualLRP.State, + "cell_id": actualLRP.CellId, + "instance_guid": actualLRP.InstanceGuid, + "modification_tag_index": actualLRP.ModificationTag.Index, + "crash_count": actualLRP.CrashCount, + "crash_reason": truncateString(actualLRP.CrashReason, 1024), + "since": actualLRP.Since, + "net_info": netInfoData, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-to-crash-actual-lrp", err) + return err + } + + return nil + }) + + return &beforeActualLRP, actualLRP, immediateRestart, err +} + +func (db *SQLDB) FailActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, placementError string) (*models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("db-fail-actual-lrp", lager.Data{"actual_lrp_key": key, "placement_error": placementError}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeActualLRP models.ActualLRP + var actualLRP *models.ActualLRP + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, tx) + if err != nil { + logger.Error("failed-to-get-actual-lrp", err) + return err + } + beforeActualLRP = *actualLRP + + if actualLRP.State != models.ActualLRPStateUnclaimed { + logger.Error("cannot-fail-actual-lrp", nil, lager.Data{"from_state": actualLRP.State}) + return models.ErrActualLRPCannotBeFailed + } + + now := db.clock.Now().UnixNano() + actualLRP.ModificationTag.Increment() + actualLRP.PlacementError = placementError + actualLRP.Since = now + + _, err = db.update(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "modification_tag_index": actualLRP.ModificationTag.Index, + "placement_error": truncateString(actualLRP.PlacementError, 1024), + "since": actualLRP.Since, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + key.ProcessGuid, key.Index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-failing-actual-lrp", err) + return err + } + + return nil + }) + + return &beforeActualLRP, actualLRP, err +} + +func (db *SQLDB) RemoveActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, instanceKey *models.ActualLRPInstanceKey) error { + logger = logger.Session("db-remove-actual-lrp", lager.Data{"process_guid": processGuid, "index": index}) + logger.Info("starting") + defer logger.Info("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + var result sql.Result + if instanceKey == nil { + result, err = db.delete(ctx, logger, tx, actualLRPsTable, + "process_guid = ? AND instance_index = ? AND presence = ?", + processGuid, index, models.ActualLRP_Ordinary, + ) + } else { + result, err = db.delete(ctx, logger, tx, actualLRPsTable, + "process_guid = ? AND instance_index = ? AND presence = ? AND instance_guid = ? AND cell_id = ?", + processGuid, index, models.ActualLRP_Ordinary, instanceKey.InstanceGuid, instanceKey.CellId, + ) + } + if err != nil { + logger.Error("failed-removing-actual-lrp", err) + return err + } + + numRows, err := result.RowsAffected() + if err != nil { + logger.Error("failed-getting-rows-affected", err) + return err + } + if numRows == 0 { + logger.Debug("not-found", lager.Data{"instance_key": instanceKey}) + return models.ErrResourceNotFound + } + + return nil + }) +} + +func (db *SQLDB) createRunningActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, instanceKey *models.ActualLRPInstanceKey, netInfo *models.ActualLRPNetInfo, internalRoutes []*models.ActualLRPInternalRoute, metricTags map[string]string, routable bool, availabilityZone string, tx helpers.Tx) (*models.ActualLRP, error) { + now := db.clock.Now().UnixNano() + guid, err := db.guidProvider.NextGUID() + if err != nil { + return nil, models.ErrGUIDGeneration + } + + actualLRP := &models.ActualLRP{} + actualLRP.ModificationTag = models.NewModificationTag(guid, 0) + actualLRP.ActualLRPKey = *key + actualLRP.ActualLRPInstanceKey = *instanceKey + actualLRP.ActualLRPNetInfo = *netInfo + actualLRP.ActualLrpInternalRoutes = internalRoutes + actualLRP.MetricTags = metricTags + actualLRP.State = models.ActualLRPStateRunning + actualLRP.Since = now + actualLRP.SetRoutable(routable) + actualLRP.AvailabilityZone = availabilityZone + + netInfoData, err := db.serializeModel(logger, &actualLRP.ActualLRPNetInfo) + if err != nil { + return nil, err + } + + internalRoutesData, err := db.encodeInternalRouteData(logger, internalRoutes) + if err != nil { + logger.Error("failed-to-serialize-internalroutes", err) + return nil, err + } + + metricTagsData, err := db.encodeMetricTagsData(logger, metricTags) + if err != nil { + logger.Error("failed-to-serialize-metric-tags", err) + return nil, err + } + + _, err = db.insert(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "process_guid": actualLRP.ActualLRPKey.ProcessGuid, + "instance_index": actualLRP.ActualLRPKey.Index, + "domain": actualLRP.ActualLRPKey.Domain, + "instance_guid": actualLRP.ActualLRPInstanceKey.InstanceGuid, + "cell_id": actualLRP.ActualLRPInstanceKey.CellId, + "state": actualLRP.State, + "net_info": netInfoData, + "internal_routes": internalRoutesData, + "metric_tags": metricTagsData, + "routable": actualLRP.GetRoutable(), + "availability_zone": actualLRP.AvailabilityZone, + "since": actualLRP.Since, + "modification_tag_epoch": actualLRP.ModificationTag.Epoch, + "modification_tag_index": actualLRP.ModificationTag.Index, + }, + ) + if err != nil { + logger.Error("failed-creating-running-actual-lrp", err) + return nil, err + } + return actualLRP, nil +} + +func (db *SQLDB) unclaimActualLRP(ctx context.Context, logger lager.Logger, tx helpers.Tx, processGuid string, index int32, isStale bool) (*models.ActualLRP, *models.ActualLRP, error) { + var beforeActualLRP models.ActualLRP + var actualLRP *models.ActualLRP + var err error + + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Ordinary, tx) + if err != nil { + logger.Error("failed-fetching-actual-lrp-for-share", err) + return nil, nil, err + } + beforeActualLRP = *actualLRP + + if actualLRP.State == models.ActualLRPStateUnclaimed { + logger.Debug("already-" + actualLRP.State) + return nil, nil, models.ErrActualLRPCannotBeUnclaimed + } + if isStale && actualLRP.State == models.ActualLRPStateClaimed { + logger.Debug("a stale unstarted claim already-" + actualLRP.State + " by another cell.") + return nil, nil, models.ErrActualLRPCannotBeUnclaimed + } + + now := db.clock.Now().UnixNano() + actualLRP.ModificationTag.Increment() + actualLRP.State = models.ActualLRPStateUnclaimed + actualLRP.ActualLRPInstanceKey.CellId = "" + actualLRP.ActualLRPInstanceKey.InstanceGuid = "" + actualLRP.Since = now + actualLRP.ActualLRPNetInfo = models.ActualLRPNetInfo{} + netInfoData, err := db.serializeModel(logger, &models.ActualLRPNetInfo{}) + if err != nil { + logger.Error("failed-to-serialize-net-info", err) + return nil, nil, err + } + + _, err = db.update(ctx, logger, tx, actualLRPsTable, + helpers.SQLAttributes{ + "state": actualLRP.State, + "cell_id": actualLRP.CellId, + "instance_guid": actualLRP.InstanceGuid, + "modification_tag_index": actualLRP.ModificationTag.Index, + "since": actualLRP.Since, + "net_info": netInfoData, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + actualLRP.ProcessGuid, actualLRP.Index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-to-unclaim-actual-lrp", err) + return nil, nil, err + } + + return &beforeActualLRP, actualLRP, nil +} + +func (db *SQLDB) scanToActualLRP(logger lager.Logger, row helpers.RowScanner) (*models.ActualLRP, error) { + var netInfoData []byte + var internalRoutesData []byte + var metricTagsData []byte + var routable bool + var actualLRP models.ActualLRP + + err := row.Scan( + &actualLRP.ProcessGuid, + &actualLRP.Index, + &actualLRP.Presence, + &actualLRP.Domain, + &actualLRP.State, + &actualLRP.InstanceGuid, + &actualLRP.CellId, + &actualLRP.PlacementError, + &actualLRP.Since, + &netInfoData, + &internalRoutesData, + &metricTagsData, + &routable, + &actualLRP.AvailabilityZone, + &actualLRP.ModificationTag.Epoch, + &actualLRP.ModificationTag.Index, + &actualLRP.CrashCount, + &actualLRP.CrashReason, + ) + if err != nil { + logger.Error("failed-scanning-actual-lrp", err) + return nil, err + } + + if len(netInfoData) > 0 { + err = db.deserializeModel(logger, netInfoData, &actualLRP.ActualLRPNetInfo) + if err != nil { + logger.Error("failed-unmarshaling-net-info-data", err) + return &actualLRP, models.ErrDeserialize + } + } + + internalRoutes := []*models.ActualLRPInternalRoute{} + if len(internalRoutesData) > 0 { + decodedData, err := db.encoder.Decode(internalRoutesData) + if err != nil { + logger.Error("failed-decrypting-internal-routes", err) + return &actualLRP, models.ErrDeserialize + } + err = json.Unmarshal(decodedData, &internalRoutes) + if err != nil { + logger.Error("failed-parsing-internal-routes", err) + return &actualLRP, models.ErrDeserialize + } + } + actualLRP.ActualLrpInternalRoutes = internalRoutes + + metricTags := map[string]string{} + if len(metricTagsData) > 0 { + decodedData, err := db.encoder.Decode(metricTagsData) + if err != nil { + logger.Error("failed-decrypting-metric-tags", err) + return &actualLRP, models.ErrDeserialize + } + err = json.Unmarshal(decodedData, &metricTags) + if err != nil { + logger.Error("failed-parsing-metric-tags", err) + return &actualLRP, models.ErrDeserialize + } + } + actualLRP.MetricTags = metricTags + actualLRP.SetRoutable(routable) + + return &actualLRP, nil +} + +func (db *SQLDB) fetchActualLRPForUpdate(ctx context.Context, logger lager.Logger, processGuid string, index int32, presence models.ActualLRP_Presence, tx helpers.Tx) (*models.ActualLRP, error) { + wheres := "process_guid = ? AND instance_index = ? AND presence = ?" + bindings := []interface{}{processGuid, index, presence} + + rows, err := db.all(ctx, logger, tx, actualLRPsTable, + actualLRPColumns, helpers.LockRow, wheres, bindings...) + if err != nil { + logger.Error("failed-query", err) + return nil, err + } + actualLRPs, err := db.scanAndCleanupActualLRPs(ctx, logger, tx, rows) + if err != nil { + return nil, err + } + + if len(actualLRPs) == 0 { + return nil, models.ErrResourceNotFound + } + + if len(actualLRPs) > 1 { + return nil, models.ErrResourceConflict + } + + return actualLRPs[0], nil +} + +func (db *SQLDB) scanAndCleanupActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable, rows *sql.Rows) ([]*models.ActualLRP, error) { + result := []*models.ActualLRP{} + actualsToDelete := []*models.ActualLRP{} + + for rows.Next() { + actualLRP, err := db.scanToActualLRP(logger, rows) + if err == models.ErrDeserialize { + actualsToDelete = append(actualsToDelete, actualLRP) + continue + } else if err != nil { + logger.Error("failed-scanning-actual-lrp", err) + return nil, err + } + + result = append(result, actualLRP) + } + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + return nil, db.convertSQLError(rows.Err()) + } + + for _, actual := range actualsToDelete { + _, err := db.delete(ctx, logger, q, actualLRPsTable, + "process_guid = ? AND instance_index = ? AND presence = ?", + actual.ProcessGuid, actual.Index, actual.Presence, + ) + if err != nil { + logger.Error("failed-cleaning-up-invalid-actual-lrp", err) + } + } + + return result, nil +} + +func (db *SQLDB) encodeInternalRouteData(logger lager.Logger, routes []*models.ActualLRPInternalRoute) ([]byte, error) { + routeData, err := json.Marshal(routes) + if err != nil { + logger.Error("failed-marshalling-routes", err) + return nil, models.ErrBadRequest + } + encodedData, err := db.encoder.Encode(routeData) + if err != nil { + logger.Error("failed-encrypting-routes", err) + return nil, models.ErrBadRequest + } + return encodedData, nil +} + +func (db *SQLDB) encodeMetricTagsData(logger lager.Logger, tags map[string]string) ([]byte, error) { + tagsData, err := json.Marshal(tags) + if err != nil { + logger.Error("failed-marshalling-metric-tags", err) + return nil, models.ErrBadRequest + } + encodedData, err := db.encoder.Encode(tagsData) + if err != nil { + logger.Error("failed-encrypting-metric-tags", err) + return nil, models.ErrBadRequest + } + return encodedData, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/bbs_health_check.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/bbs_health_check.go new file mode 100644 index 0000000000..a66255b59b --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/bbs_health_check.go @@ -0,0 +1,32 @@ +package sqldb + +import ( + "context" + "fmt" + "time" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) PerformBBSHealthCheck(ctx context.Context, logger lager.Logger, t time.Time) error { + logger = logger.Session("db") + logger.Debug("starting") + defer logger.Debug("done") + + logger.Debug("upserting-time", lager.Data{"time": t}) + _, err := db.upsert(ctx, logger, db.db, "bbs_health_check", helpers.SQLAttributes{"id": 1, "time": t.UnixNano()}, "id = ?", 1) + if err != nil { + return fmt.Errorf("failed upserting health check time: %s", err) + } + + logger.Debug("retrieving-upserted-time") + scanner := db.db.QueryRowContext(ctx, helpers.RebindForFlavor("SELECT time from bbs_health_check where id = ?", db.flavor), 1) + var insertedTime int64 + err = scanner.Scan(&insertedTime) + if err != nil { + return fmt.Errorf("failed querying for health check time: %s", err) + } + logger.Debug("upserted-and-retrieved-time") + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/configuration_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/configuration_db.go new file mode 100644 index 0000000000..312e13f83e --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/configuration_db.go @@ -0,0 +1,41 @@ +package sqldb + +import ( + "context" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const configurationsTable = "configurations" + +func (db *SQLDB) setConfigurationValue(tx helpers.Tx, ctx context.Context, logger lager.Logger, key, value string) error { + _, err := db.upsert( + ctx, + logger, + tx, + configurationsTable, + helpers.SQLAttributes{"value": value, "id": key}, + "id = ?", key, + ) + if err != nil { + logger.Error("failed-setting-config-value", err, lager.Data{"key": key}) + return err + } + + return nil +} + +func (db *SQLDB) getConfigurationValue(tx helpers.Tx, ctx context.Context, logger lager.Logger, key string) (string, error) { + var value string + + err := db.one(ctx, logger, tx, "configurations", + helpers.ColumnList{"value"}, helpers.NoLockRow, + "id = ?", key, + ).Scan(&value) + if err != nil { + return "", err + } + + return value, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/desired_lrp_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/desired_lrp_db.go new file mode 100644 index 0000000000..8f0d8e425b --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/desired_lrp_db.go @@ -0,0 +1,699 @@ +package sqldb + +import ( + "context" + "database/sql" + "encoding/json" + "strings" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) DesireLRP(ctx context.Context, logger lager.Logger, desiredLRP *models.DesiredLRP) error { + logger = logger.Session("db-desire-lrp", lager.Data{"process_guid": desiredLRP.ProcessGuid}) + logger.Info("starting") + defer logger.Info("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + routesData, err := db.encodeRouteData(logger, desiredLRP.Routes) + if err != nil { + logger.Error("failed-encoding-route-data", err) + return err + } + + metricTagsData, err := db.encodeDesiredMetricTagsData(logger, desiredLRP.MetricTags) + if err != nil { + logger.Error("failed-encoding-metric-tags-data", err) + return err + } + + runInfo := desiredLRP.DesiredLRPRunInfo(db.clock.Now()) + + runInfoData, err := db.serializeModel(logger, &runInfo) + if err != nil { + logger.Error("failed-to-serialize-model", err) + return err + } + + volumePlacement := &models.VolumePlacement{} + volumePlacement.DriverNames = []string{} + for _, mount := range desiredLRP.VolumeMounts { + volumePlacement.DriverNames = append(volumePlacement.DriverNames, mount.Driver) + } + + volumePlacementData, err := db.serializeModel(logger, volumePlacement) + if err != nil { + logger.Error("failed-to-serialize-model", err) + return err + } + + guid, err := db.guidProvider.NextGUID() + if err != nil { + logger.Error("failed-to-generate-guid", err) + return models.ErrGUIDGeneration + } + + placementTagData, err := json.Marshal(desiredLRP.PlacementTags) + if err != nil { + logger.Error("failed-to-serialize-model", err) + return err + } + + desiredLRP.ModificationTag = &models.ModificationTag{Epoch: guid, Index: 0} + + _, err = db.insert(ctx, logger, tx, desiredLRPsTable, + helpers.SQLAttributes{ + "process_guid": desiredLRP.ProcessGuid, + "domain": desiredLRP.Domain, + "log_guid": desiredLRP.LogGuid, + "annotation": desiredLRP.Annotation, + "instances": desiredLRP.Instances, + "memory_mb": desiredLRP.MemoryMb, + "disk_mb": desiredLRP.DiskMb, + "max_pids": desiredLRP.MaxPids, + "rootfs": desiredLRP.RootFs, + "volume_placement": volumePlacementData, + "modification_tag_epoch": desiredLRP.ModificationTag.Epoch, + "modification_tag_index": desiredLRP.ModificationTag.Index, + "routes": routesData, + "run_info": runInfoData, + "placement_tags": placementTagData, + "metric_tags": metricTagsData, + "update_strategy": desiredLRP.UpdateStrategy, + }, + ) + if err != nil { + logger.Error("failed-inserting-desired", err) + return err + } + return nil + }) +} + +func (db *SQLDB) DesiredLRPByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (*models.DesiredLRP, error) { + logger = logger.Session("db-desired-lrp-by-process-guid", lager.Data{"process_guid": processGuid}) + logger.Debug("starting") + defer logger.Debug("complete") + + var desiredLRP *models.DesiredLRP + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + row := db.one(ctx, logger, tx, desiredLRPsTable, + desiredLRPColumns, helpers.NoLockRow, + "process_guid = ?", processGuid, + ) + + desiredLRP, _, err = db.fetchDesiredLRP(ctx, logger, row, tx) + return err + }) + + return desiredLRP, err +} + +func (db *SQLDB) DesiredLRPs(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRP, error) { + logger = logger.Session("db-desired-lrps", lager.Data{"filter": filter}) + logger.Debug("start") + defer logger.Debug("complete") + + var wheres []string + var values []interface{} + + if len(filter.AppGuids) > 0 { + var appGuidWheres []string + for _, g := range filter.AppGuids { + appGuidWheres = append(appGuidWheres, "process_guid LIKE ?") + values = append(values, g+"%") + } + if len(filter.AppGuids) == 1 { + wheres = append(wheres, appGuidWheres[0]) + } else { + wheres = append(wheres, "("+strings.Join(appGuidWheres, " OR ")+")") + } + } + + if filter.Domain != "" { + wheres = append(wheres, "domain = ?") + values = append(values, filter.Domain) + } + + if len(filter.ProcessGuids) > 0 { + wheres = append(wheres, whereClauseForProcessGuids(filter.ProcessGuids)) + + for _, guid := range filter.ProcessGuids { + values = append(values, guid) + } + } + + results := []*models.DesiredLRP{} + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + rows, err := db.all(ctx, logger, tx, desiredLRPsTable, + desiredLRPColumns, helpers.NoLockRow, + strings.Join(wheres, " AND "), values..., + ) + if err != nil { + logger.Error("failed-query", err) + return err + } + defer rows.Close() + + results, err = db.fetchDesiredLRPs(ctx, logger, rows, tx) + if err != nil { + logger.Error("failed-fetching-row", rows.Err()) + return db.convertSQLError(rows.Err()) + } + + return nil + }) + + return results, err +} + +func (db *SQLDB) DesiredLRPSchedulingInfos(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRPSchedulingInfo, error) { + logger = logger.Session("db-desired-lrps-scheduling-infos", lager.Data{"filter": filter}) + logger.Debug("starting") + defer logger.Debug("complete") + + var wheres []string + var values []interface{} + + if filter.Domain != "" { + wheres = append(wheres, "domain = ?") + values = append(values, filter.Domain) + } + + if len(filter.ProcessGuids) > 0 { + wheres = append(wheres, whereClauseForProcessGuids(filter.ProcessGuids)) + + for _, guid := range filter.ProcessGuids { + values = append(values, guid) + } + } + + results := []*models.DesiredLRPSchedulingInfo{} + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + rows, err := db.all(ctx, logger, tx, desiredLRPsTable, + schedulingInfoColumns, helpers.NoLockRow, + strings.Join(wheres, " AND "), values..., + ) + if err != nil { + logger.Error("failed-query", err) + return err + } + defer rows.Close() + + for rows.Next() { + desiredLRPSchedulingInfo, err := db.fetchDesiredLRPSchedulingInfo(logger, rows) + if err != nil { + logger.Error("failed-reading-row", err) + continue + } + results = append(results, desiredLRPSchedulingInfo) + } + + if rows.Err() != nil { + logger.Error("failed-fetching-row", rows.Err()) + return db.convertSQLError(rows.Err()) + } + + return nil + }) + + return results, err +} + +func (db *SQLDB) DesiredLRPSchedulingInfoByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (*models.DesiredLRPSchedulingInfo, error) { + logger = logger.Session("db-desired-lrp-scheduling-info-by-process-guid", lager.Data{"process_guid": processGuid}) + logger.Debug("starting") + defer logger.Debug("complete") + + var desiredLRPSchedulingInfo *models.DesiredLRPSchedulingInfo + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + row := db.one(ctx, logger, tx, desiredLRPsTable, + schedulingInfoColumns, helpers.NoLockRow, + "process_guid = ?", processGuid, + ) + + desiredLRPSchedulingInfo, err = db.fetchDesiredLRPSchedulingInfo(logger, row) + return err + }) + + return desiredLRPSchedulingInfo, err +} + +func (db *SQLDB) DesiredLRPUpdateStrategyByProcessGuid(ctx context.Context, logger lager.Logger, processGuid string) (models.DesiredLRP_UpdateStrategy, int32, error) { + logger = logger.Session("db-desired-lrp-update-strategy-by-process-guid", lager.Data{"process_guid": processGuid}) + logger.Debug("starting") + defer logger.Debug("complete") + + var updateStrategy models.DesiredLRP_UpdateStrategy + var instances int32 + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + row := db.one(ctx, logger, tx, desiredLRPsTable, + helpers.ColumnList{"update_strategy", "instances"}, helpers.NoLockRow, + "process_guid = ?", processGuid, + ) + err := row.Scan(&updateStrategy, &instances) + if err != nil { + logger.Error("failed-scanning-row", err) + return db.convertSQLError(err) + } + return nil + }) + + return updateStrategy, instances, err +} + +func (db *SQLDB) DesiredLRPRoutingInfos(ctx context.Context, logger lager.Logger, filter models.DesiredLRPFilter) ([]*models.DesiredLRP, error) { + logger = logger.Session("db-desired-lrps-routing-infos", lager.Data{"filter": filter}) + logger.Debug("starting") + defer logger.Debug("complete") + + var wheres []string + var values []interface{} + + if len(filter.ProcessGuids) > 0 { + wheres = append(wheres, whereClauseForProcessGuids(filter.ProcessGuids)) + + for _, guid := range filter.ProcessGuids { + values = append(values, guid) + } + } + + results := []*models.DesiredLRP{} + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + rows, err := db.all(ctx, logger, tx, desiredLRPsTable, + routingInfoColumns, helpers.NoLockRow, + strings.Join(wheres, " AND "), values..., + ) + if err != nil { + logger.Error("failed-query", err) + return err + } + defer rows.Close() + + for rows.Next() { + desiredLRPRoutingInfo, err := db.fetchDesiredLRPRoutingInfo(logger, rows) + if err != nil { + logger.Error("failed-reading-row", err) + continue + } + results = append(results, desiredLRPRoutingInfo) + } + + if rows.Err() != nil { + logger.Error("failed-fetching-row", rows.Err()) + return db.convertSQLError(rows.Err()) + } + + return nil + }) + + return results, err +} + +func (db *SQLDB) UpdateDesiredLRP(ctx context.Context, logger lager.Logger, processGuid string, update *models.DesiredLRPUpdate) (*models.DesiredLRP, error) { + logger = logger.Session("db-update-desired-lrp", lager.Data{"process_guid": processGuid}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeDesiredLRP *models.DesiredLRP + var originalRunInfo *models.DesiredLRPRunInfo + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + row := db.one(ctx, logger, tx, desiredLRPsTable, + desiredLRPColumns, helpers.LockRow, + "process_guid = ?", processGuid, + ) + beforeDesiredLRP, originalRunInfo, err = db.fetchDesiredLRP(ctx, logger, row, tx) + + if err != nil { + logger.Error("failed-lock-desired", err) + return err + } + + updateAttributes := helpers.SQLAttributes{"modification_tag_index": beforeDesiredLRP.ModificationTag.Index + 1} + + if update.AnnotationExists() { + updateAttributes["annotation"] = update.GetAnnotation() + } + + if update.InstancesExists() { + updateAttributes["instances"] = update.GetInstances() + } + + if update.Routes != nil { + encodedData, err := db.encodeRouteData(logger, update.Routes) + if err != nil { + return err + } + updateAttributes["routes"] = encodedData + } + + if update.MetricTags != nil { + encodedData, err := db.encodeDesiredMetricTagsData(logger, update.MetricTags) + if err != nil { + return err + } + updateAttributes["metric_tags"] = encodedData + } + + if update.ImageUsernameExists() || update.ImagePasswordExists() { + runInfo := *originalRunInfo + + if update.ImageUsernameExists() { + runInfo.ImageUsername = update.GetImageUsername() + } + if update.ImagePasswordExists() { + runInfo.ImagePassword = update.GetImagePassword() + } + + updatedRunInfoData, err := db.serializeModel(logger, &runInfo) + if err != nil { + logger.Error("failed-serializing-run-info", err) + return err + } + updateAttributes["run_info"] = updatedRunInfoData + } + + _, err = db.update(ctx, logger, tx, desiredLRPsTable, updateAttributes, `process_guid = ?`, processGuid) + if err != nil { + logger.Error("failed-executing-query", err) + return err + } + + return nil + }) + + return beforeDesiredLRP, err +} + +func (db *SQLDB) encodeRouteData(logger lager.Logger, routes *models.Routes) ([]byte, error) { + routeData, err := json.Marshal(routes) + if err != nil { + logger.Error("failed-marshalling-routes", err) + return nil, models.ErrBadRequest + } + encodedData, err := db.encoder.Encode(routeData) + if err != nil { + logger.Error("failed-encrypting-routes", err) + return nil, models.ErrBadRequest + } + return encodedData, nil +} + +func (db *SQLDB) encodeDesiredMetricTagsData(logger lager.Logger, metricTags map[string]*models.MetricTagValue) ([]byte, error) { + metricTagsData, err := json.Marshal(metricTags) + if err != nil { + logger.Error("failed-marshalling-routes", err) + return nil, models.ErrBadRequest + } + encodedData, err := db.encoder.Encode(metricTagsData) + if err != nil { + logger.Error("failed-encrypting-routes", err) + return nil, models.ErrBadRequest + } + return encodedData, nil +} + +func (db *SQLDB) RemoveDesiredLRP(ctx context.Context, logger lager.Logger, processGuid string) error { + logger = logger.Session("db-remove-desired-lrp", lager.Data{"process_guid": processGuid}) + logger.Info("starting") + defer logger.Info("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + err := db.lockDesiredLRPByGuidForUpdate(ctx, logger, processGuid, tx) + if err != nil { + logger.Error("failed-lock-desired", err) + return err + } + + _, err = db.delete(ctx, logger, tx, desiredLRPsTable, "process_guid = ?", processGuid) + if err != nil { + logger.Error("failed-deleting-from-db", err) + return err + } + + return nil + }) +} + +// "rows" needs to have the columns defined in the schedulingInfoColumns constant +func (db *SQLDB) fetchDesiredLRPSchedulingInfoAndMore(logger lager.Logger, scanner helpers.RowScanner, dest ...interface{}) (*models.DesiredLRPSchedulingInfo, error) { + schedulingInfo := &models.DesiredLRPSchedulingInfo{} + var routeData, volumePlacementData, placementTagData []byte + values := []interface{}{ + &schedulingInfo.ProcessGuid, + &schedulingInfo.Domain, + &schedulingInfo.LogGuid, + &schedulingInfo.Annotation, + &schedulingInfo.Instances, + &schedulingInfo.MemoryMb, + &schedulingInfo.DiskMb, + &schedulingInfo.MaxPids, + &schedulingInfo.RootFs, + &routeData, + &volumePlacementData, + &schedulingInfo.ModificationTag.Epoch, + &schedulingInfo.ModificationTag.Index, + &placementTagData, + } + values = append(values, dest...) + + err := scanner.Scan(values...) + if err == sql.ErrNoRows { + return nil, err + } + + if err != nil { + logger.Error("failed-scanning", err) + return nil, err + } + + var routes models.Routes + encodedData, err := db.encoder.Decode(routeData) + if err != nil { + logger.Error("failed-decrypting-routes", err) + return nil, err + } + err = json.Unmarshal(encodedData, &routes) + if err != nil { + logger.Error("failed-parsing-routes", err) + return nil, err + } + schedulingInfo.Routes = routes + + var volumePlacement models.VolumePlacement + err = db.deserializeModel(logger, volumePlacementData, &volumePlacement) + if err != nil { + logger.Error("failed-parsing-volume-placement", err) + return nil, err + } + schedulingInfo.VolumePlacement = &volumePlacement + if placementTagData != nil { + err = json.Unmarshal(placementTagData, &schedulingInfo.PlacementTags) + if err != nil { + logger.Error("failed-parsing-placement-tags", err) + return nil, err + } + } + + return schedulingInfo, nil +} + +func (db *SQLDB) fetchDesiredLRPRoutingInfo(logger lager.Logger, scanner helpers.RowScanner, dest ...interface{}) (*models.DesiredLRP, error) { + routingInfo := &models.DesiredLRP{} + var modificationTagEpoch string + var modificationTagIndex uint32 + var routeData, metricTagsData []byte + values := []interface{}{ + &routingInfo.ProcessGuid, + &routingInfo.Domain, + &routingInfo.LogGuid, + &routingInfo.Instances, + &routeData, + &modificationTagEpoch, + &modificationTagIndex, + &metricTagsData, + } + + err := scanner.Scan(values...) + if err == sql.ErrNoRows { + return nil, err + } + + if err != nil { + logger.Error("failed-scanning", err) + return nil, err + } + var routes models.Routes + encodedData, err := db.encoder.Decode(routeData) + if err != nil { + logger.Error("failed-decrypting-routes", err) + return nil, err + } + err = json.Unmarshal(encodedData, &routes) + if err != nil { + logger.Error("failed-parsing-routes", err) + return nil, err + } + routingInfo.Routes = &routes + + var metricTags map[string]*models.MetricTagValue + decodedDesiredData, err := db.encoder.Decode(metricTagsData) + if err != nil { + logger.Error("failed-decrypting-metric-tags", err) + return nil, err + } + err = json.Unmarshal(decodedDesiredData, &metricTags) + if err != nil { + logger.Error("failed-parsing-metric-tags", err) + return nil, err + } + routingInfo.MetricTags = metricTags + routingInfo.ModificationTag = &models.ModificationTag{Epoch: modificationTagEpoch, Index: modificationTagIndex} + + return routingInfo, nil +} + +func (db *SQLDB) lockDesiredLRPByGuidForUpdate(ctx context.Context, logger lager.Logger, processGuid string, tx helpers.Tx) error { + row := db.one(ctx, logger, tx, desiredLRPsTable, + helpers.ColumnList{"1"}, helpers.LockRow, + "process_guid = ?", processGuid, + ) + var count int + err := row.Scan(&count) + if err != nil { + return err + } + return nil +} + +func (db *SQLDB) fetchDesiredLRPs(ctx context.Context, logger lager.Logger, rows *sql.Rows, queryable helpers.Queryable) ([]*models.DesiredLRP, error) { + guids := []string{} + lrps := []*models.DesiredLRP{} + for rows.Next() { + lrp, _, guid, err := db.fetchDesiredLRPInternal(logger, rows) + if err == models.ErrDeserialize { + guids = append(guids, guid) + } + if err != nil { + logger.Error("failed-reading-row", err) + continue + } + lrps = append(lrps, lrp) + } + + if len(guids) > 0 { + deleteErr := db.deleteInvalidLRPs(ctx, logger, queryable, guids...) + if deleteErr != nil { + logger.Error("failed-to-delete-invalid-lrps", deleteErr, lager.Data{"guid": guids}) + } + } + + if err := rows.Err(); err != nil { + return lrps, err + } + + return lrps, nil +} + +func (db *SQLDB) fetchDesiredLRP(ctx context.Context, logger lager.Logger, scanner helpers.RowScanner, queryable helpers.Queryable) (*models.DesiredLRP, *models.DesiredLRPRunInfo, error) { + lrp, runInfo, guid, err := db.fetchDesiredLRPInternal(logger, scanner) + if err == models.ErrDeserialize { + deleteErr := db.deleteInvalidLRPs(ctx, logger, queryable, guid) + if deleteErr != nil { + logger.Error("failed-to-delete-invalid-lrp", deleteErr, lager.Data{"guid": guid}) + } + } + return lrp, runInfo, err +} + +func (db *SQLDB) fetchDesiredLRPInternal(logger lager.Logger, scanner helpers.RowScanner) (*models.DesiredLRP, *models.DesiredLRPRunInfo, string, error) { + var runInfoData, metricTagsData []byte + var updateStrategy models.DesiredLRP_UpdateStrategy + schedulingInfo, err := db.fetchDesiredLRPSchedulingInfoAndMore(logger, scanner, &runInfoData, &metricTagsData, &updateStrategy) + if err != nil { + return nil, nil, "", err + } + + var runInfo models.DesiredLRPRunInfo + err = db.deserializeModel(logger, runInfoData, &runInfo) + if err != nil { + return nil, nil, schedulingInfo.ProcessGuid, models.ErrDeserialize + } + // dedup the ports + runInfo.Ports = dedupSlice(runInfo.Ports) + + var metricTags map[string]*models.MetricTagValue + encodedData, err := db.encoder.Decode(metricTagsData) + if err != nil { + logger.Error("failed-decrypting-metric-tags", err) + return nil, nil, "", err + } + err = json.Unmarshal(encodedData, &metricTags) + if err != nil { + logger.Error("failed-parsing-metric-tags", err) + return nil, nil, "", err + } + desiredLRP := models.NewDesiredLRP(*schedulingInfo, runInfo, metricTags, updateStrategy) + return &desiredLRP, &runInfo, "", nil +} + +func (db *SQLDB) deleteInvalidLRPs(ctx context.Context, logger lager.Logger, queryable helpers.Queryable, guids ...string) error { + for _, guid := range guids { + logger.Info("deleting-invalid-desired-lrp-from-db", lager.Data{"guid": guid}) + _, err := db.delete(ctx, logger, queryable, desiredLRPsTable, "process_guid = ?", guid) + if err != nil { + logger.Error("failed-deleting-invalid-row", err) + return err + } + } + return nil +} + +func (db *SQLDB) fetchDesiredLRPSchedulingInfo(logger lager.Logger, scanner helpers.RowScanner) (*models.DesiredLRPSchedulingInfo, error) { + return db.fetchDesiredLRPSchedulingInfoAndMore(logger, scanner) +} + +func whereClauseForProcessGuids(filter []string) string { + var questionMarks []string + + where := "process_guid IN (" + for range filter { + questionMarks = append(questionMarks, "?") + } + + where += strings.Join(questionMarks, ", ") + return where + ")" +} + +func dedupSlice(ints []uint32) []uint32 { + if ints == nil { + // this is really here to make some tests happy, otherwise we replace the + // nil with an empty slice and they barf + return nil + } + + set := make(map[uint32]struct{}) + for _, i := range ints { + set[i] = struct{}{} + } + if len(ints) == len(set) { + // short circuit the copying if the set has the same number of elements as + // the slice + return ints + } + + newIs := make([]uint32, 0, len(ints)) + for i := range set { + newIs = append(newIs, i) + } + return newIs +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/domain_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/domain_db.go new file mode 100644 index 0000000000..e2ef082300 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/domain_db.go @@ -0,0 +1,103 @@ +package sqldb + +import ( + "context" + "math" + "time" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) FreshDomains(ctx context.Context, logger lager.Logger) ([]string, error) { + logger = logger.Session("db-fresh-domains") + logger.Debug("starting") + defer logger.Debug("complete") + + var domainNames []string + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + expireTime := db.clock.Now().Round(time.Second) + domains, err := db.domains(ctx, logger, tx, expireTime) + if err != nil { + return err + } + + domainNames = nil + for _, d := range domains { + domainNames = append(domainNames, d.name) + } + return nil + }) + + return domainNames, err +} + +type domain struct { + name string + expiresAt time.Time +} + +func (db *SQLDB) domains(ctx context.Context, logger lager.Logger, tx helpers.Queryable, expiresAfter time.Time) ([]domain, error) { + rows, err := db.all(ctx, logger, tx, domainsTable, + domainColumns, helpers.NoLockRow, + "expire_time > ?", + expiresAfter.UnixNano(), + ) + if err != nil { + logger.Error("failed-query", err) + return nil, err + } + + defer rows.Close() + + var results []domain + + for rows.Next() { + var name string + var expiresAt int64 + err = rows.Scan(&name, &expiresAt) + if err != nil { + logger.Error("failed-scan-row", err) + return nil, err + } + + results = append(results, domain{name, time.Unix(0, int64(expiresAt))}) + } + + if rows.Err() != nil { + logger.Error("failed-fetching-row", err) + return nil, err + } + + return results, nil +} + +func (db *SQLDB) UpsertDomain(ctx context.Context, logger lager.Logger, domain string, ttl uint32) error { + logger = logger.Session("db-upsert-domain", lager.Data{"domain": domain, "ttl": ttl}) + logger.Debug("starting") + defer logger.Debug("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + expireTime := db.clock.Now().Add(time.Duration(ttl) * time.Second).UnixNano() + if ttl == 0 { + expireTime = math.MaxInt64 + } + + ok, err := db.upsert(ctx, logger, tx, domainsTable, + helpers.SQLAttributes{"domain": domain, "expire_time": expireTime}, + "domain = ?", domain, + ) + + if err != nil { + logger.Error("failed-inserting-domain", err) + return err + } + + if ok { + logger.Info("added-domain", lager.Data{"domain": domain}) + } + + return nil + }) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/encryption_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/encryption_db.go new file mode 100644 index 0000000000..f964990719 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/encryption_db.go @@ -0,0 +1,227 @@ +package sqldb + +import ( + "context" + "fmt" + "strings" + + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const EncryptionKeyID = "encryption_key_label" + +func (db *SQLDB) SetEncryptionKeyLabel(ctx context.Context, logger lager.Logger, label string) error { + logger = logger.Session("db-set-encryption-key-label", lager.Data{"label": label}) + logger.Debug("starting") + defer logger.Debug("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + return db.setConfigurationValue(tx, ctx, logger, EncryptionKeyID, label) + }) +} + +func (db *SQLDB) EncryptionKeyLabel(ctx context.Context, logger lager.Logger) (string, error) { + logger = logger.Session("db-encryption-key-label") + logger.Debug("starting") + defer logger.Debug("complete") + + var ekLabel string + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var getErr error + ekLabel, getErr = db.getConfigurationValue(tx, ctx, logger, EncryptionKeyID) + return getErr + }) + if err != nil { + return "", err + } + return ekLabel, nil +} + +func (db *SQLDB) PerformEncryption(ctx context.Context, logger lager.Logger) error { + errCh := make(chan error) + + funcs := []func(){ + func() { + errCh <- db.reEncrypt(ctx, logger, encryptable{ + TableName: tasksTable, + PrimaryKeyNames: []string{"guid"}, + Columns: []string{"task_definition"}, + EncryptIfEmpty: true, + PrimaryKeyFunc: func() primaryKey { return &taskPrimaryKey{} }, + }) + }, + func() { + errCh <- db.reEncrypt(ctx, logger, encryptable{ + TableName: desiredLRPsTable, + PrimaryKeyNames: []string{"process_guid"}, + Columns: []string{"run_info", "volume_placement", "routes", "metric_tags"}, + EncryptIfEmpty: true, + PrimaryKeyFunc: func() primaryKey { return &desiredLRPPrimaryKey{} }, + }) + }, + func() { + errCh <- db.reEncrypt(ctx, logger, encryptable{ + TableName: actualLRPsTable, + PrimaryKeyNames: []string{"process_guid", "instance_index", "presence"}, + Columns: []string{"net_info", "internal_routes", "metric_tags"}, + EncryptIfEmpty: false, + PrimaryKeyFunc: func() primaryKey { return &actualLRPPrimaryKey{} }, + }) + }, + } + + for _, f := range funcs { + go f() + } + + for range funcs { + err := <-errCh + if err != nil { + return err + } + } + return nil +} + +func (db *SQLDB) reEncrypt(ctx context.Context, logger lager.Logger, toEncrypt encryptable) error { + logger = logger.WithData( + lager.Data{"table_name": toEncrypt.TableName, "primary_key": toEncrypt.PrimaryKeyNames, "blob_columns": toEncrypt.Columns}, + ) + rows, err := db.db.QueryContext(ctx, fmt.Sprintf("SELECT %s FROM %s", strings.Join(toEncrypt.PrimaryKeyNames, ", "), toEncrypt.TableName)) + if err != nil { + return err + } + defer rows.Close() + + pks := []primaryKey{} + for rows.Next() { + pk := toEncrypt.PrimaryKeyFunc() + err := pk.Scan(rows) + if err != nil { + logger.Error("failed-to-scan-primary-key", err) + continue + } + pks = append(pks, pk) + } + + whereClauses := []string{} + for _, name := range toEncrypt.PrimaryKeyNames { + whereClauses = append(whereClauses, name+" = ?") + + } + where := strings.Join(whereClauses, " AND ") + + for _, pk := range pks { + err = db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + blobs := make([]interface{}, len(toEncrypt.Columns)) + + row := db.one(ctx, logger, tx, toEncrypt.TableName, toEncrypt.Columns, helpers.LockRow, where, pk.WhereBindings()...) + for i := range toEncrypt.Columns { + var blob []byte + blobs[i] = &blob + } + + err := row.Scan(blobs...) + if err != nil { + logger.Error("failed-to-scan-blob", err) + return nil + } + + updatedColumnValues := map[string]interface{}{} + + for columnIdx := range blobs { + // This type assertion should not fail because we set the value to be a pointer to a byte array above + blobPtr := blobs[columnIdx].(*[]byte) + blob := *blobPtr + + // don't encrypt column if it doesn't contain any data, see #132626553 for more info + if !toEncrypt.EncryptIfEmpty && len(blob) == 0 { + return nil + } + + encoder := format.NewEncoder(db.cryptor) + payload, err := encoder.Decode(blob) + if err != nil { + logger.Error("failed-to-decode-blob", err) + return nil + } + encryptedPayload, err := encoder.Encode(payload) + if err != nil { + logger.Error("failed-to-encode-blob", err) + return err + } + + columnName := toEncrypt.Columns[columnIdx] + updatedColumnValues[columnName] = encryptedPayload + } + _, err = db.update(ctx, logger, tx, toEncrypt.TableName, + updatedColumnValues, + where, pk.WhereBindings()..., + ) + if err != nil { + logger.Error("failed-to-update-blob", err) + return err + } + return nil + }) + + if err != nil { + return err + } + } + + return nil +} + +type encryptable struct { + TableName string + PrimaryKeyNames []string + Columns []string + EncryptIfEmpty bool + PrimaryKeyFunc func() primaryKey +} + +type primaryKey interface { + Scan(row helpers.RowScanner) error + WhereBindings() []interface{} +} + +type actualLRPPrimaryKey struct { + ProcessGuid string + InstanceIndex int32 + Presence string +} + +func (pk *actualLRPPrimaryKey) Scan(row helpers.RowScanner) error { + return row.Scan(&pk.ProcessGuid, &pk.InstanceIndex, &pk.Presence) +} + +func (pk *actualLRPPrimaryKey) WhereBindings() []interface{} { + return []interface{}{pk.ProcessGuid, pk.InstanceIndex, pk.Presence} +} + +type desiredLRPPrimaryKey struct { + ProcessGuid string +} + +func (pk *desiredLRPPrimaryKey) Scan(row helpers.RowScanner) error { + return row.Scan(&pk.ProcessGuid) +} + +func (pk *desiredLRPPrimaryKey) WhereBindings() []interface{} { + return []interface{}{pk.ProcessGuid} +} + +type taskPrimaryKey struct { + Guid string +} + +func (pk *taskPrimaryKey) Scan(row helpers.RowScanner) error { + return row.Scan(&pk.Guid) +} + +func (pk *taskPrimaryKey) WhereBindings() []interface{} { + return []interface{}{pk.Guid} +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/evacuation_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/evacuation_db.go new file mode 100644 index 0000000000..c34f5a08d2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/evacuation_db.go @@ -0,0 +1,225 @@ +package sqldb + +import ( + "context" + "reflect" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) EvacuateActualLRP( + ctx context.Context, + logger lager.Logger, + lrpKey *models.ActualLRPKey, + instanceKey *models.ActualLRPInstanceKey, + netInfo *models.ActualLRPNetInfo, + internalRoutes []*models.ActualLRPInternalRoute, + metricTags map[string]string, + routable bool, + availabilityZone string, +) (*models.ActualLRP, error) { + logger = logger.Session("db-evacuate-actual-lrp", lager.Data{"lrp_key": lrpKey, "instance_key": instanceKey, "net_info": netInfo, "routable": routable}) + logger.Debug("starting") + defer logger.Debug("complete") + + var actualLRP *models.ActualLRP + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + processGuid := lrpKey.ProcessGuid + index := lrpKey.Index + + actualLRP, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Evacuating, tx) + if err == models.ErrResourceNotFound { + logger.Debug("creating-evacuating-lrp") + actualLRP, err = db.createEvacuatingActualLRP(ctx, logger, lrpKey, instanceKey, netInfo, internalRoutes, metricTags, routable, availabilityZone, tx) + return err + } + + if err != nil { + logger.Error("failed-locking-lrp", err) + return err + } + + if actualLRP.ActualLRPKey.Equal(lrpKey) && + actualLRP.ActualLRPInstanceKey.Equal(instanceKey) && + reflect.DeepEqual(actualLRP.ActualLRPNetInfo, *netInfo) { + logger.Debug("evacuating-lrp-already-exists") + return models.ErrResourceExists + } + + now := db.clock.Now().UnixNano() + actualLRP.ModificationTag.Increment() + actualLRP.ActualLRPKey = *lrpKey + actualLRP.ActualLRPInstanceKey = *instanceKey + actualLRP.Since = now + actualLRP.ActualLRPNetInfo = *netInfo + actualLRP.ActualLrpInternalRoutes = internalRoutes + actualLRP.MetricTags = metricTags + actualLRP.AvailabilityZone = availabilityZone + actualLRP.Presence = models.ActualLRP_Evacuating + + netInfoData, err := db.serializeModel(logger, netInfo) + if err != nil { + logger.Error("failed-serializing-net-info", err) + return err + } + + internalRoutesData, err := db.encodeInternalRouteData(logger, internalRoutes) + if err != nil { + logger.Error("failed-to-serialize-internalroutes", err) + return err + } + + metricTagsData, err := db.encodeMetricTagsData(logger, metricTags) + if err != nil { + logger.Error("failed-to-serialize-metric-tags", err) + return err + } + + _, err = db.update(ctx, logger, tx, "actual_lrps", + helpers.SQLAttributes{ + "domain": actualLRP.Domain, + "instance_guid": actualLRP.InstanceGuid, + "cell_id": actualLRP.CellId, + "net_info": netInfoData, + "internal_routes": internalRoutesData, + "metric_tags": metricTagsData, + "state": actualLRP.State, + "since": actualLRP.Since, + "modification_tag_index": actualLRP.ModificationTag.Index, + }, + "process_guid = ? AND instance_index = ? AND presence = ?", + actualLRP.ProcessGuid, actualLRP.Index, models.ActualLRP_Evacuating, + ) + if err != nil { + logger.Error("failed-update-evacuating-lrp", err) + return err + } + + return nil + }) + + return actualLRP, err +} + +func (db *SQLDB) RemoveEvacuatingActualLRP(ctx context.Context, logger lager.Logger, lrpKey *models.ActualLRPKey, instanceKey *models.ActualLRPInstanceKey) error { + logger = logger.Session("db-remove-evacuating-actual-lrp", lager.Data{"lrp_key": lrpKey, "instance_key": instanceKey}) + logger.Debug("starting") + defer logger.Debug("complete") + + return db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + processGuid := lrpKey.ProcessGuid + index := lrpKey.Index + + lrp, err := db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Evacuating, tx) + if err == models.ErrResourceNotFound { + logger.Debug("evacuating-lrp-does-not-exist") + return nil + } + + if err != nil { + logger.Error("failed-fetching-actual-lrp", err) + return err + } + + if !lrp.ActualLRPInstanceKey.Equal(instanceKey) { + logger.Debug("actual-lrp-instance-key-mismatch", lager.Data{"instance_key_param": instanceKey, "instance_key_from_db": lrp.ActualLRPInstanceKey}) + return models.ErrActualLRPCannotBeRemoved + } + + _, err = db.delete(ctx, logger, tx, "actual_lrps", + "process_guid = ? AND instance_index = ? AND presence = ?", + processGuid, index, models.ActualLRP_Evacuating, + ) + if err != nil { + logger.Error("failed-delete", err) + return models.ErrActualLRPCannotBeRemoved + } + + return nil + }) +} + +func (db *SQLDB) createEvacuatingActualLRP( + ctx context.Context, + logger lager.Logger, + lrpKey *models.ActualLRPKey, + instanceKey *models.ActualLRPInstanceKey, + netInfo *models.ActualLRPNetInfo, + internalRoutes []*models.ActualLRPInternalRoute, + metricTags map[string]string, + routable bool, + availabilityZone string, + tx helpers.Tx, +) (*models.ActualLRP, error) { + netInfoData, err := db.serializeModel(logger, netInfo) + if err != nil { + logger.Error("failed-serializing-net-info", err) + return nil, err + } + + internalRoutesData, err := db.encodeInternalRouteData(logger, internalRoutes) + if err != nil { + logger.Error("failed-to-serialize-internalroutes", err) + return nil, err + } + + metricTagsData, err := db.encodeMetricTagsData(logger, metricTags) + if err != nil { + logger.Error("failed-to-serialize-metric-tags", err) + return nil, err + } + + now := db.clock.Now() + guid, err := db.guidProvider.NextGUID() + if err != nil { + return nil, models.ErrGUIDGeneration + } + + actualLRP := &models.ActualLRP{ + ActualLRPKey: *lrpKey, + ActualLRPInstanceKey: *instanceKey, + ActualLRPNetInfo: *netInfo, + ActualLrpInternalRoutes: internalRoutes, + MetricTags: metricTags, + AvailabilityZone: availabilityZone, + State: models.ActualLRPStateRunning, + Since: now.UnixNano(), + ModificationTag: models.ModificationTag{Epoch: guid, Index: 0}, + Presence: models.ActualLRP_Evacuating, + } + actualLRP.SetRoutable(routable) + + sqlAttributes := helpers.SQLAttributes{ + "process_guid": actualLRP.ProcessGuid, + "instance_index": actualLRP.Index, + "presence": models.ActualLRP_Evacuating, + "domain": actualLRP.Domain, + "instance_guid": actualLRP.InstanceGuid, + "cell_id": actualLRP.CellId, + "state": actualLRP.State, + "net_info": netInfoData, + "internal_routes": internalRoutesData, + "metric_tags": metricTagsData, + "routable": routable, + "availability_zone": availabilityZone, + "since": actualLRP.Since, + "modification_tag_epoch": actualLRP.ModificationTag.Epoch, + "modification_tag_index": actualLRP.ModificationTag.Index, + } + + _, err = db.upsert(ctx, logger, tx, "actual_lrps", + sqlAttributes, + "process_guid = ? AND instance_index = ? AND presence = ?", + actualLRP.ProcessGuid, actualLRP.Index, models.ActualLRP_Evacuating, + ) + if err != nil { + logger.Error("failed-inserting-evacuating-lrp", err) + return nil, err + } + + return actualLRP, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/lrp_convergence.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/lrp_convergence.go new file mode 100644 index 0000000000..03e7fb417f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/lrp_convergence.go @@ -0,0 +1,630 @@ +package sqldb + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/routing-info/internalroutes" +) + +func (sqldb *SQLDB) ConvergeLRPs(ctx context.Context, logger lager.Logger, cellSet models.CellSet) db.ConvergenceResult { + logger = logger.Session("db-converge-lrps") + logger.Info("starting") + defer logger.Info("complete") + + now := sqldb.clock.Now() + sqldb.pruneDomains(ctx, logger, now) + events, instanceEvents := sqldb.pruneEvacuatingActualLRPs(ctx, logger, cellSet) + domainSet, err := sqldb.domainSet(ctx, logger) + if err != nil { + return db.ConvergenceResult{} + } + + converge := newConvergence(sqldb) + converge.staleUnclaimedActualLRPs(ctx, logger, now) + converge.actualLRPsWithMissingCells(ctx, logger, cellSet) + converge.lrpInstanceCounts(ctx, logger, domainSet) + converge.orphanedActualLRPs(ctx, logger) + converge.orphanedSuspectActualLRPs(ctx, logger) + converge.extraSuspectActualLRPs(ctx, logger) + converge.suspectActualLRPsWithExistingCells(ctx, logger, cellSet) + converge.suspectRunningActualLRPs(ctx, logger) + converge.suspectClaimedActualLRPs(ctx, logger) + converge.crashedActualLRPs(ctx, logger, now) + converge.lrpsWithInternalRouteChanges(ctx, logger) + converge.lrpsWithMetricTagChanges(ctx, logger) + + return db.ConvergenceResult{ + MissingLRPKeys: converge.missingLRPKeys, + UnstartedLRPKeys: converge.unstartedLRPKeys, + KeysToRetire: converge.keysToRetire, + SuspectLRPKeysToRetire: converge.suspectKeysToRetire, + KeysWithMissingCells: converge.ordinaryKeysWithMissingCells, + MissingCellIds: converge.missingCellIds, + Events: events, + InstanceEvents: instanceEvents, + SuspectKeysWithExistingCells: converge.suspectKeysWithExistingCells, + SuspectRunningKeys: converge.suspectRunningKeys, + SuspectClaimedKeys: converge.suspectClaimedKeys, + KeysWithInternalRouteChanges: converge.keysWithInternalRouteChanges, + KeysWithMetricTagChanges: converge.keysWithMetricTagChanges, + } +} + +type convergence struct { + *SQLDB + + ordinaryKeysWithMissingCells []*models.ActualLRPKeyWithSchedulingInfo + missingCellIds []string + suspectKeysWithExistingCells []*models.ActualLRPKey + + suspectKeysToRetire []*models.ActualLRPKey + + suspectRunningKeys []*models.ActualLRPKey + suspectClaimedKeys []*models.ActualLRPKey + + keysToRetire []*models.ActualLRPKey + + missingLRPKeys []*models.ActualLRPKeyWithSchedulingInfo + + unstartedLRPKeys []*models.ActualLRPKeyWithSchedulingInfo + + keysWithInternalRouteChanges []*db.ActualLRPKeyWithInternalRoutes + keysWithMetricTagChanges []*db.ActualLRPKeyWithMetricTags +} + +func newConvergence(db *SQLDB) *convergence { + return &convergence{ + SQLDB: db, + } +} + +// Adds stale UNCLAIMED Actual LRPs to the list of start requests. +func (c *convergence) staleUnclaimedActualLRPs(ctx context.Context, logger lager.Logger, now time.Time) { + logger = logger.Session("stale-unclaimed-actual-lrps") + + rows, err := c.selectStaleUnclaimedLRPs(ctx, logger, c.db, now) + if err != nil { + logger.Error("failed-query", err) + return + } + + for rows.Next() { + var index int + schedulingInfo, err := c.fetchDesiredLRPSchedulingInfoAndMore(logger, rows, &index) + if err != nil { + continue + } + key := models.NewActualLRPKey(schedulingInfo.ProcessGuid, int32(index), schedulingInfo.Domain) + c.unstartedLRPKeys = append(c.unstartedLRPKeys, &models.ActualLRPKeyWithSchedulingInfo{ + Key: &key, + SchedulingInfo: schedulingInfo, + }) + logger.Info("creating-start-request", + lager.Data{"reason": "stale-unclaimed-lrp", "process_guid": schedulingInfo.ProcessGuid, "index": index}) + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + +} + +// Adds CRASHED Actual LRPs that can be restarted to the list of start requests +// and transitions them to UNCLAIMED. +func (c *convergence) crashedActualLRPs(ctx context.Context, logger lager.Logger, now time.Time) { + logger = logger.Session("crashed-actual-lrps") + restartCalculator := models.NewDefaultRestartCalculator() + + rows, err := c.selectCrashedLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + for rows.Next() { + var index int + actual := &models.ActualLRP{} + + schedulingInfo, err := c.fetchDesiredLRPSchedulingInfoAndMore(logger, rows, &index, &actual.Since, &actual.CrashCount) + if err != nil { + continue + } + + actual.ActualLRPKey = models.NewActualLRPKey(schedulingInfo.ProcessGuid, int32(index), schedulingInfo.Domain) + actual.State = models.ActualLRPStateCrashed + + if actual.ShouldRestartCrash(now, restartCalculator) { + c.unstartedLRPKeys = append(c.unstartedLRPKeys, &models.ActualLRPKeyWithSchedulingInfo{ + Key: &actual.ActualLRPKey, + SchedulingInfo: schedulingInfo, + }) + logger.Info("creating-start-request", + lager.Data{"reason": "crashed-instance", "process_guid": actual.ProcessGuid, "index": index}) + } + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + +} + +func (c *convergence) lrpsWithInternalRouteChanges(ctx context.Context, logger lager.Logger) { + logger = logger.Session("lrps-with-internal-route-changes") + rows, err := c.selectLRPsWithRoutes(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + for rows.Next() { + actualLRPKey := &models.ActualLRPKey{} + actualLRPInstanceKey := &models.ActualLRPInstanceKey{} + var actualRouteData []byte + var desiredRouteData []byte + + values := []interface{}{ + &actualLRPKey.ProcessGuid, + &actualLRPKey.Index, + &actualLRPKey.Domain, + &actualLRPInstanceKey.InstanceGuid, + &actualLRPInstanceKey.CellId, + &actualRouteData, + &desiredRouteData, + } + + err := rows.Scan(values...) + if err == sql.ErrNoRows { + continue + } + + if err != nil { + logger.Error("failed-scanning", err) + continue + } + + var desiredRoutes models.Routes + decodedDesiredData, err := c.encoder.Decode(desiredRouteData) + if err != nil { + logger.Error("failed-decrypting-desired-routes", err) + continue + } + err = json.Unmarshal(decodedDesiredData, &desiredRoutes) + if err != nil { + logger.Error("failed-parsing-desired-routes", err) + continue + } + + actualInternalRoutes := internalroutes.InternalRoutes{} + if len(actualRouteData) > 0 { + decodedActualData, err := c.encoder.Decode(actualRouteData) + if err != nil { + logger.Error("failed-decrypting-actual-routes", err) + continue + } + err = json.Unmarshal(decodedActualData, &actualInternalRoutes) + if err != nil { + logger.Error("failed-parsing-actual-routes", err) + continue + } + } + + desiredInternalRoutes, err := internalroutes.InternalRoutesFromRoutingInfo(desiredRoutes) + if err != nil { + logger.Error("failed-getting-internal-routes-from-desired", err) + continue + } + + if !actualInternalRoutes.Equal(desiredInternalRoutes) { + c.keysWithInternalRouteChanges = append(c.keysWithInternalRouteChanges, &db.ActualLRPKeyWithInternalRoutes{ + Key: actualLRPKey, + InstanceKey: actualLRPInstanceKey, + DesiredInternalRoutes: desiredInternalRoutes, + }) + } + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + +} + +func (c *convergence) lrpsWithMetricTagChanges(ctx context.Context, logger lager.Logger) { + logger = logger.Session("lrps-with-metric-tag-changes") + rows, err := c.selectLRPsWithMetricTags(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + for rows.Next() { + actualLRPKey := &models.ActualLRPKey{} + actualLRPInstanceKey := &models.ActualLRPInstanceKey{} + var actualMetricTagData []byte + var desiredMetricTagData []byte + + values := []interface{}{ + &actualLRPKey.ProcessGuid, + &actualLRPKey.Index, + &actualLRPKey.Domain, + &actualLRPInstanceKey.InstanceGuid, + &actualLRPInstanceKey.CellId, + &actualMetricTagData, + &desiredMetricTagData, + } + + err := rows.Scan(values...) + if err == sql.ErrNoRows { + continue + } + + if err != nil { + logger.Error("failed-scanning", err) + continue + } + + var metricTags map[string]*models.MetricTagValue + decodedDesiredData, err := c.encoder.Decode(desiredMetricTagData) + if err != nil { + logger.Error("failed-decrypting-desired-metric-tags", err) + continue + } + err = json.Unmarshal(decodedDesiredData, &metricTags) + if err != nil { + logger.Error("failed-parsing-desired-metric-tags", err) + continue + } + desiredMetricTags, err := models.ConvertMetricTags(metricTags, map[models.MetricTagValue_DynamicValue]interface{}{ + models.MetricTagDynamicValueIndex: actualLRPKey.Index, + models.MetricTagDynamicValueInstanceGuid: actualLRPInstanceKey.InstanceGuid, + }) + if err != nil { + logger.Error("converting-metric-tags-failed", err) + continue + } + + var actualMetricTags map[string]string + if len(actualMetricTagData) > 0 { + decodedActualData, err := c.encoder.Decode(actualMetricTagData) + if err != nil { + logger.Error("failed-decrypting-actual-metric-tags", err) + continue + } + err = json.Unmarshal(decodedActualData, &actualMetricTags) + if err != nil { + logger.Error("failed-parsing-actual-metric-tags", err) + continue + } + } + + if actualMetricTags != nil && !reflect.DeepEqual(desiredMetricTags, actualMetricTags) { + c.keysWithMetricTagChanges = append(c.keysWithMetricTagChanges, &db.ActualLRPKeyWithMetricTags{ + Key: actualLRPKey, + InstanceKey: actualLRPInstanceKey, + DesiredMetricTags: desiredMetricTags, + }) + } + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + +} + +func scanActualLRPs(logger lager.Logger, rows *sql.Rows) []*models.ActualLRPKey { + var actualLRPKeys []*models.ActualLRPKey + for rows.Next() { + actualLRPKey := &models.ActualLRPKey{} + + err := rows.Scan( + &actualLRPKey.ProcessGuid, + &actualLRPKey.Index, + &actualLRPKey.Domain, + ) + if err != nil { + logger.Error("failed-scanning", err) + continue + } + + actualLRPKeys = append(actualLRPKeys, actualLRPKey) + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + return actualLRPKeys +} + +// Adds orphaned Actual LRPs (ones with no corresponding Desired LRP) to the +// list of keys to retire. +func (c *convergence) orphanedActualLRPs(ctx context.Context, logger lager.Logger) { + logger = logger.Session("orphaned-actual-lrps") + + rows, err := c.selectOrphanedActualLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.keysToRetire = append(c.keysToRetire, scanActualLRPs(logger, rows)...) +} + +func (c *convergence) extraSuspectActualLRPs(ctx context.Context, logger lager.Logger) { + logger = logger.Session("extra-suspect-lrps") + + rows, err := c.selectExtraSuspectActualLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.suspectKeysToRetire = append(c.suspectKeysToRetire, scanActualLRPs(logger, rows)...) +} + +func (c *convergence) orphanedSuspectActualLRPs(ctx context.Context, logger lager.Logger) { + logger = logger.Session("orphaned-suspect-lrps") + + rows, err := c.selectOrphanedSuspectActualLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.suspectKeysToRetire = append(c.suspectKeysToRetire, scanActualLRPs(logger, rows)...) +} + +func (c *convergence) suspectRunningActualLRPs(ctx context.Context, logger lager.Logger) { + logger = logger.Session("suspect-running-lrps") + + rows, err := c.selectSuspectRunningActualLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.suspectRunningKeys = scanActualLRPs(logger, rows) +} + +func (c *convergence) suspectClaimedActualLRPs(ctx context.Context, logger lager.Logger) { + logger = logger.Session("suspect-running-lrps") + + rows, err := c.selectSuspectClaimedActualLRPs(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.suspectClaimedKeys = scanActualLRPs(logger, rows) +} + +// Creates and adds missing Actual LRPs to the list of start requests. +// Adds extra Actual LRPs to the list of keys to retire. +func (c *convergence) lrpInstanceCounts(ctx context.Context, logger lager.Logger, domainSet map[string]struct{}) { + logger = logger.Session("lrp-instance-counts") + + rows, err := c.selectLRPInstanceCounts(ctx, logger, c.db) + if err != nil { + logger.Error("failed-query", err) + return + } + + for rows.Next() { + var existingIndicesStr sql.NullString + var actualInstances int + + schedulingInfo, err := c.fetchDesiredLRPSchedulingInfoAndMore(logger, rows, &actualInstances, &existingIndicesStr) + if err != nil { + continue + } + + existingIndices := make(map[int]struct{}) + if existingIndicesStr.String != "" { + for _, indexStr := range strings.Split(existingIndicesStr.String, ",") { + index, err := strconv.Atoi(indexStr) + if err != nil { + logger.Error("cannot-parse-index", err, lager.Data{ + "index": indexStr, + "existing-indices-str": existingIndicesStr, + }) + return + } + existingIndices[index] = struct{}{} + } + } + + for i := 0; i < int(schedulingInfo.Instances); i++ { + _, found := existingIndices[i] + if found { + continue + } + + index := int32(i) + c.missingLRPKeys = append(c.missingLRPKeys, &models.ActualLRPKeyWithSchedulingInfo{ + Key: &models.ActualLRPKey{ + ProcessGuid: schedulingInfo.ProcessGuid, + Domain: schedulingInfo.Domain, + Index: index, + }, + SchedulingInfo: schedulingInfo, + }) + logger.Info("creating-start-request", + lager.Data{"reason": "missing-instance", "process_guid": schedulingInfo.ProcessGuid, "index": index}) + } + + for index := range existingIndices { + if index < int(schedulingInfo.Instances) { + continue + } + + // only take destructive actions for fresh domains + if _, ok := domainSet[schedulingInfo.Domain]; ok { + c.keysToRetire = append(c.keysToRetire, &models.ActualLRPKey{ + ProcessGuid: schedulingInfo.ProcessGuid, + Index: int32(index), + Domain: schedulingInfo.Domain, + }) + } + } + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } +} + +// Unclaim Actual LRPs that have missing cells (not in the cell set passed to +// convergence) and add them to the list of start requests. +func (c *convergence) suspectActualLRPsWithExistingCells(ctx context.Context, logger lager.Logger, cellSet models.CellSet) { + logger = logger.Session("suspect-lrps-with-existing-cells") + + if len(cellSet) == 0 { + return + } + + rows, err := c.selectSuspectLRPsWithExistingCells(ctx, logger, c.db, cellSet) + if err != nil { + logger.Error("failed-query", err) + return + } + + c.suspectKeysWithExistingCells = scanActualLRPs(logger, rows) +} + +// Unclaim Actual LRPs that have missing cells (not in the cell set passed to +// convergence) and add them to the list of start requests. +func (c *convergence) actualLRPsWithMissingCells(ctx context.Context, logger lager.Logger, cellSet models.CellSet) { + logger = logger.Session("actual-lrps-with-missing-cells") + + var ordinaryKeysWithMissingCells []*models.ActualLRPKeyWithSchedulingInfo + + rows, err := c.selectLRPsWithMissingCells(ctx, logger, c.db, cellSet) + if err != nil { + logger.Error("failed-query", err) + return + } + + missingCellSet := make(map[string]struct{}) + for rows.Next() { + var index int32 + var cellID string + var presence models.ActualLRP_Presence + schedulingInfo, err := c.fetchDesiredLRPSchedulingInfoAndMore(logger, rows, &index, &cellID, &presence) + if err == nil && presence == models.ActualLRP_Ordinary { + ordinaryKeysWithMissingCells = append(ordinaryKeysWithMissingCells, &models.ActualLRPKeyWithSchedulingInfo{ + Key: &models.ActualLRPKey{ + ProcessGuid: schedulingInfo.ProcessGuid, + Domain: schedulingInfo.Domain, + Index: index, + }, + SchedulingInfo: schedulingInfo, + }) + } + missingCellSet[cellID] = struct{}{} + } + + if rows.Err() != nil { + logger.Error("failed-getting-next-row", rows.Err()) + } + + for key := range missingCellSet { + c.missingCellIds = append(c.missingCellIds, key) + } + + if len(c.missingCellIds) > 0 { + logger.Info("detected-missing-cells", lager.Data{"cell_ids": c.missingCellIds}) + } + + c.ordinaryKeysWithMissingCells = ordinaryKeysWithMissingCells +} + +func (db *SQLDB) pruneDomains(ctx context.Context, logger lager.Logger, now time.Time) { + logger = logger.Session("prune-domains") + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + domains, err := db.domains(ctx, logger, tx, time.Time{}) + if err != nil { + return err + } + + for _, d := range domains { + if d.expiresAt.After(now) { + continue + } + + logger.Info("pruning-domain", lager.Data{"domain": d.name, "expire-at": d.expiresAt}) + _, err := db.delete(ctx, logger, tx, domainsTable, "domain = ? ", d.name) + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + logger.Error("cannot-prune-domains", err) + } +} + +func (db *SQLDB) pruneEvacuatingActualLRPs(ctx context.Context, logger lager.Logger, cellSet models.CellSet) ([]models.Event, []models.Event) { + logger = logger.Session("prune-evacuating-actual-lrps") + + wheres := []string{"presence = ?"} + bindings := []interface{}{models.ActualLRP_Evacuating} + + if len(cellSet) > 0 { + wheres = append(wheres, fmt.Sprintf("actual_lrps.cell_id NOT IN (%s)", helpers.QuestionMarks(len(cellSet)))) + + for cellID := range cellSet { + bindings = append(bindings, cellID) + } + } + + lrpsToDelete, err := db.getActualLRPs(ctx, logger, strings.Join(wheres, " AND "), bindings...) + if err != nil { + logger.Error("failed-fetching-evacuating-lrps-with-missing-cells", err) + } + + _, err = db.delete(ctx, logger, db.db, actualLRPsTable, strings.Join(wheres, " AND "), bindings...) + if err != nil { + logger.Error("failed-query", err) + } + + var events []models.Event + var instanceEvents []models.Event + for _, lrp := range lrpsToDelete { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + events = append(events, models.NewActualLRPRemovedEvent(lrp.ToActualLRPGroup())) + instanceEvents = append(instanceEvents, models.NewActualLRPInstanceRemovedEvent(lrp, trace.RequestIdFromContext(ctx))) + } + return events, instanceEvents +} + +func (db *SQLDB) domainSet(ctx context.Context, logger lager.Logger) (map[string]struct{}, error) { + logger.Debug("listing-domains") + domains, err := db.FreshDomains(ctx, logger) + if err != nil { + logger.Error("failed-listing-domains", err) + return nil, err + } + logger.Debug("succeeded-listing-domains") + m := make(map[string]struct{}, len(domains)) + for _, domain := range domains { + m[domain] = struct{}{} + } + return m, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/package.go new file mode 100644 index 0000000000..5b61b8af1e --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/package.go @@ -0,0 +1 @@ +package sqldb // import "code.cloudfoundry.org/bbs/db/sqldb" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/queries.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/queries.go new file mode 100644 index 0000000000..a12c0eb689 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/queries.go @@ -0,0 +1,416 @@ +package sqldb + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const ( + tasksTable = "tasks" + desiredLRPsTable = "desired_lrps" + actualLRPsTable = "actual_lrps" + domainsTable = "domains" +) + +var ( + routingInfoColumns = helpers.ColumnList{ + desiredLRPsTable + ".process_guid", + desiredLRPsTable + ".domain", + desiredLRPsTable + ".log_guid", + desiredLRPsTable + ".instances", + desiredLRPsTable + ".routes", + desiredLRPsTable + ".modification_tag_epoch", + desiredLRPsTable + ".modification_tag_index", + desiredLRPsTable + ".metric_tags", + } + + schedulingInfoColumns = helpers.ColumnList{ + desiredLRPsTable + ".process_guid", + desiredLRPsTable + ".domain", + desiredLRPsTable + ".log_guid", + desiredLRPsTable + ".annotation", + desiredLRPsTable + ".instances", + desiredLRPsTable + ".memory_mb", + desiredLRPsTable + ".disk_mb", + desiredLRPsTable + ".max_pids", + desiredLRPsTable + ".rootfs", + desiredLRPsTable + ".routes", + desiredLRPsTable + ".volume_placement", + desiredLRPsTable + ".modification_tag_epoch", + desiredLRPsTable + ".modification_tag_index", + desiredLRPsTable + ".placement_tags", + } + + desiredLRPColumns = append(schedulingInfoColumns, + desiredLRPsTable+".run_info", + desiredLRPsTable+".metric_tags", + desiredLRPsTable+".update_strategy", + ) + + taskColumns = helpers.ColumnList{ + tasksTable + ".guid", + tasksTable + ".domain", + tasksTable + ".updated_at", + tasksTable + ".created_at", + tasksTable + ".first_completed_at", + tasksTable + ".state", + tasksTable + ".cell_id", + tasksTable + ".result", + tasksTable + ".failed", + tasksTable + ".failure_reason", + tasksTable + ".task_definition", + tasksTable + ".rejection_count", + tasksTable + ".rejection_reason", + } + + actualLRPColumns = helpers.ColumnList{ + actualLRPsTable + ".process_guid", + actualLRPsTable + ".instance_index", + actualLRPsTable + ".presence", + actualLRPsTable + ".domain", + actualLRPsTable + ".state", + actualLRPsTable + ".instance_guid", + actualLRPsTable + ".cell_id", + actualLRPsTable + ".placement_error", + actualLRPsTable + ".since", + actualLRPsTable + ".net_info", + actualLRPsTable + ".internal_routes", + actualLRPsTable + ".metric_tags", + actualLRPsTable + ".routable", + actualLRPsTable + ".availability_zone", + actualLRPsTable + ".modification_tag_epoch", + actualLRPsTable + ".modification_tag_index", + actualLRPsTable + ".crash_count", + actualLRPsTable + ".crash_reason", + } + + actualLRPIDColumns = helpers.ColumnList{ + actualLRPsTable + ".process_guid", + actualLRPsTable + ".instance_index", + actualLRPsTable + ".domain", + actualLRPsTable + ".instance_guid", + actualLRPsTable + ".cell_id", + } + + domainColumns = helpers.ColumnList{ + domainsTable + ".domain", + domainsTable + ".expire_time", + } +) + +func (db *SQLDB) CreateConfigurationsTable(ctx context.Context, logger lager.Logger) error { + _, err := db.db.ExecContext( + ctx, + `CREATE TABLE IF NOT EXISTS configurations( + id VARCHAR(255) PRIMARY KEY, + value VARCHAR(255) + ) + `) + if err != nil { + return err + } + + return nil +} + +func (db *SQLDB) selectLRPInstanceCounts(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + var query string + columns := schedulingInfoColumns + columns = append(columns, "COUNT(actual_lrps.instance_index) AS actual_instances") + + switch db.flavor { + case helpers.Postgres: + columns = append(columns, "STRING_AGG(actual_lrps.instance_index::text, ',') AS existing_indices") + case helpers.MySQL: + columns = append(columns, "GROUP_CONCAT(actual_lrps.instance_index) AS existing_indices") + default: + // totally shouldn't happen + panic("database flavor not implemented: " + db.flavor) + } + + query = fmt.Sprintf(` + SELECT %s + FROM desired_lrps + LEFT OUTER JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid AND actual_lrps.presence = %d + GROUP BY desired_lrps.process_guid + HAVING COUNT(actual_lrps.instance_index) <> desired_lrps.instances + `, + strings.Join(columns, ", "), models.ActualLRP_Ordinary, + ) + + return q.QueryContext(ctx, query) +} + +func (db *SQLDB) selectOrphanedActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT actual_lrps.process_guid, actual_lrps.instance_index, actual_lrps.domain + FROM actual_lrps + JOIN domains ON actual_lrps.domain = domains.domain + LEFT JOIN desired_lrps ON actual_lrps.process_guid = desired_lrps.process_guid + WHERE actual_lrps.presence = %d AND desired_lrps.process_guid IS NULL + `, models.ActualLRP_Ordinary) + + return q.QueryContext(ctx, query) +} + +func (db *SQLDB) selectOrphanedSuspectActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT actual_lrps.process_guid, actual_lrps.instance_index, actual_lrps.domain + FROM actual_lrps + JOIN domains ON actual_lrps.domain = domains.domain + LEFT JOIN desired_lrps ON actual_lrps.process_guid = desired_lrps.process_guid + WHERE actual_lrps.presence = %d AND desired_lrps.process_guid IS NULL + `, models.ActualLRP_Suspect) + + return q.QueryContext(ctx, query) +} + +func (db *SQLDB) selectSuspectRunningActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := db.helper.Rebind(`SELECT process_guid, instance_index, domain + FROM actual_lrps + WHERE actual_lrps.presence = ? AND actual_lrps.state = ?`) + + return q.QueryContext(ctx, query, models.ActualLRP_Suspect, models.ActualLRPStateRunning) +} + +func (db *SQLDB) selectSuspectClaimedActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := db.helper.Rebind(`SELECT process_guid, instance_index, domain + FROM actual_lrps + WHERE actual_lrps.presence = ? AND actual_lrps.state = ?`) + + return q.QueryContext(ctx, query, models.ActualLRP_Suspect, models.ActualLRPStateClaimed) +} + +func (db *SQLDB) selectExtraSuspectActualLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := db.helper.Rebind(`SELECT process_guid, instance_index, domain + FROM actual_lrps + WHERE actual_lrps.presence IN (?, ?) AND actual_lrps.state = ? + GROUP BY process_guid, instance_index, domain + HAVING count(*) >= 2`) + return q.QueryContext(ctx, query, models.ActualLRP_Ordinary, models.ActualLRP_Suspect, models.ActualLRPStateRunning) +} + +func (db *SQLDB) selectSuspectLRPsWithExistingCells(ctx context.Context, logger lager.Logger, q helpers.Queryable, cellSet models.CellSet) (*sql.Rows, error) { + wheres := []string{fmt.Sprintf("actual_lrps.presence = %d", models.ActualLRP_Suspect)} + bindings := make([]interface{}, 0, len(cellSet)) + + if len(cellSet) > 0 { + wheres = append(wheres, fmt.Sprintf("actual_lrps.cell_id IN (%s)", helpers.QuestionMarks(len(cellSet)))) + for cellID := range cellSet { + bindings = append(bindings, cellID) + } + } + + query := fmt.Sprintf(` + SELECT process_guid, instance_index, domain + FROM actual_lrps + WHERE %s + `, + strings.Join(wheres, " AND "), + ) + + return q.QueryContext(ctx, db.helper.Rebind(query), bindings...) +} + +func (db *SQLDB) selectLRPsWithMissingCells(ctx context.Context, logger lager.Logger, q helpers.Queryable, cellSet models.CellSet) (*sql.Rows, error) { + wheres := []string{ + "(actual_lrps.state = ? OR actual_lrps.state = ?)", + } + + bindings := []interface{}{} + + bindings = append(bindings, models.ActualLRPStateRunning, models.ActualLRPStateClaimed) + + if len(cellSet) > 0 { + wheres = append(wheres, fmt.Sprintf("actual_lrps.cell_id NOT IN (%s)", helpers.QuestionMarks(len(cellSet)))) + wheres = append(wheres, "actual_lrps.cell_id <> ''") + for cellID := range cellSet { + bindings = append(bindings, cellID) + } + } + + query := fmt.Sprintf(` + SELECT %s + FROM desired_lrps + JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid + WHERE %s + `, + strings.Join(append(schedulingInfoColumns, "actual_lrps.instance_index", "actual_lrps.cell_id", "actual_lrps.presence"), ", "), + strings.Join(wheres, " AND "), + ) + + return q.QueryContext(ctx, db.helper.Rebind(query), bindings...) +} + +func (db *SQLDB) selectCrashedLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT %s + FROM desired_lrps + JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid + WHERE actual_lrps.state = ? AND actual_lrps.presence = ? + `, + strings.Join( + append(schedulingInfoColumns, "actual_lrps.instance_index", "actual_lrps.since", "actual_lrps.crash_count"), + ", ", + ), + ) + + return q.QueryContext(ctx, db.helper.Rebind(query), models.ActualLRPStateCrashed, models.ActualLRP_Ordinary) +} + +func (db *SQLDB) selectStaleUnclaimedLRPs(ctx context.Context, logger lager.Logger, q helpers.Queryable, now time.Time) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT %s + FROM desired_lrps + JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid + WHERE actual_lrps.state = ? AND actual_lrps.since < ? AND actual_lrps.presence = ? + `, + strings.Join(append(schedulingInfoColumns, "actual_lrps.instance_index"), ", "), + ) + + return q.QueryContext(ctx, + db.helper.Rebind(query), + models.ActualLRPStateUnclaimed, + now.Add(-models.StaleUnclaimedActualLRPDuration).UnixNano(), + models.ActualLRP_Ordinary, + ) +} + +func (db *SQLDB) selectLRPsWithRoutes(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT %s + FROM desired_lrps + JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid + WHERE actual_lrps.state = ? AND actual_lrps.presence = ? + `, + strings.Join( + append(actualLRPIDColumns, actualLRPsTable+".internal_routes", desiredLRPsTable+".routes"), + ", ", + ), + ) + + return q.QueryContext(ctx, db.helper.Rebind(query), models.ActualLRPStateRunning, models.ActualLRP_Ordinary) +} + +func (db *SQLDB) selectLRPsWithMetricTags(ctx context.Context, logger lager.Logger, q helpers.Queryable) (*sql.Rows, error) { + query := fmt.Sprintf(` + SELECT %s + FROM desired_lrps + JOIN actual_lrps ON desired_lrps.process_guid = actual_lrps.process_guid + WHERE actual_lrps.state = ? AND actual_lrps.presence = ? + `, + strings.Join( + append(actualLRPIDColumns, actualLRPsTable+".metric_tags", desiredLRPsTable+".metric_tags"), + ", ", + ), + ) + + return q.QueryContext(ctx, db.helper.Rebind(query), models.ActualLRPStateRunning, models.ActualLRP_Ordinary) +} + +func (db *SQLDB) countActualLRPsForProcessGuidAndState(ctx context.Context, logger lager.Logger, processGuid string, state string, presence models.ActualLRP_Presence, q helpers.Queryable) (int, error) { + query := ` + SELECT COUNT(*) AS actual_instances + FROM actual_lrps + WHERE actual_lrps.process_guid = ? AND actual_lrps.state = ? AND actual_lrps.presence = ? + ` + + var actualInstances int + row := q.QueryRowContext(ctx, db.helper.Rebind(query), processGuid, state, presence) + err := row.Scan(&actualInstances) + if err != nil { + logger.Error("failed-actual-lrp-for-process-guid-query", err) + return 0, err + } + return actualInstances, nil +} + +func (db *SQLDB) CountDesiredInstances(ctx context.Context, logger lager.Logger) int { + query := ` + SELECT COALESCE(SUM(desired_lrps.instances), 0) AS desired_instances + FROM desired_lrps + ` + + var desiredInstances int + row := db.db.QueryRowContext(ctx, db.helper.Rebind(query)) + err := row.Scan(&desiredInstances) + if err != nil { + logger.Error("failed-desired-instances-query", err) + } + return desiredInstances +} + +func (db *SQLDB) CountActualLRPsByState(ctx context.Context, logger lager.Logger) (claimedCount, unclaimedCount, runningCount, crashedCount, crashingDesiredCount int) { + var query string + switch db.flavor { + case helpers.Postgres: + query = ` + SELECT + COUNT(*) FILTER (WHERE actual_lrps.state = $1) AS claimed_instances, + COUNT(*) FILTER (WHERE actual_lrps.state = $2) AS unclaimed_instances, + COUNT(*) FILTER (WHERE actual_lrps.state = $3) AS running_instances, + COUNT(*) FILTER (WHERE actual_lrps.state = $4) AS crashed_instances, + COUNT(DISTINCT process_guid) FILTER (WHERE actual_lrps.state = $5) AS crashing_desireds + FROM actual_lrps + WHERE presence = $6 + ` + case helpers.MySQL: + query = ` + SELECT + COUNT(IF(actual_lrps.state = ?, 1, NULL)) AS claimed_instances, + COUNT(IF(actual_lrps.state = ?, 1, NULL)) AS unclaimed_instances, + COUNT(IF(actual_lrps.state = ?, 1, NULL)) AS running_instances, + COUNT(IF(actual_lrps.state = ?, 1, NULL)) AS crashed_instances, + COUNT(DISTINCT IF(state = ?, process_guid, NULL)) AS crashing_desireds + FROM actual_lrps + WHERE presence = ? + ` + default: + // totally shouldn't happen + panic("database flavor not implemented: " + db.flavor) + } + + row := db.db.QueryRowContext(ctx, query, models.ActualLRPStateClaimed, models.ActualLRPStateUnclaimed, models.ActualLRPStateRunning, models.ActualLRPStateCrashed, models.ActualLRPStateCrashed, models.ActualLRP_Ordinary) + err := row.Scan(&claimedCount, &unclaimedCount, &runningCount, &crashedCount, &crashingDesiredCount) + if err != nil { + logger.Error("failed-counting-actual-lrps", err) + } + return +} + +func (db *SQLDB) one(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, + columns helpers.ColumnList, lockRow helpers.RowLock, + wheres string, whereBindings ...interface{}, +) helpers.RowScanner { + return db.helper.One(ctx, logger, q, table, columns, lockRow, wheres, whereBindings...) +} + +func (db *SQLDB) all(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, + columns helpers.ColumnList, lockRow helpers.RowLock, + wheres string, whereBindings ...interface{}, +) (*sql.Rows, error) { + return db.helper.All(ctx, logger, q, table, columns, lockRow, wheres, whereBindings...) +} + +func (db *SQLDB) upsert(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, attributes helpers.SQLAttributes, wheres string, whereBindings ...interface{}) (bool, error) { + return db.helper.Upsert(ctx, logger, q, table, attributes, wheres, whereBindings...) +} + +func (db *SQLDB) insert(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, attributes helpers.SQLAttributes) (sql.Result, error) { + return db.helper.Insert(ctx, logger, q, table, attributes) +} + +func (db *SQLDB) update(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, updates helpers.SQLAttributes, wheres string, whereBindings ...interface{}) (sql.Result, error) { + return db.helper.Update(ctx, logger, q, table, updates, wheres, whereBindings...) +} + +func (db *SQLDB) delete(ctx context.Context, logger lager.Logger, q helpers.Queryable, table string, wheres string, whereBindings ...interface{}) (sql.Result, error) { + return db.helper.Delete(ctx, logger, q, table, wheres, whereBindings...) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/sqldb.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/sqldb.go new file mode 100644 index 0000000000..a1e7302826 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/sqldb.go @@ -0,0 +1,101 @@ +package sqldb + +import ( + "context" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/guidprovider" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +type SQLDB struct { + db helpers.QueryableDB + convergenceWorkersSize int + updateWorkersSize int + clock clock.Clock + guidProvider guidprovider.GUIDProvider + serializer format.Serializer + cryptor encryption.Cryptor + encoder format.Encoder + flavor string + helper helpers.SQLHelper + metronClient loggingclient.IngressClient + debugStartActualLRPHeartbeats bool +} + +func NewSQLDB( + db helpers.QueryableDB, + convergenceWorkersSize int, + updateWorkersSize int, + cryptor encryption.Cryptor, + guidProvider guidprovider.GUIDProvider, + clock clock.Clock, + flavor string, + metronClient loggingclient.IngressClient, + debugStartActualLRPHeartbeats bool, +) *SQLDB { + helper := helpers.NewSQLHelper(flavor) + return &SQLDB{ + db: db, + convergenceWorkersSize: convergenceWorkersSize, + updateWorkersSize: updateWorkersSize, + clock: clock, + guidProvider: guidProvider, + serializer: format.NewSerializer(cryptor), + cryptor: cryptor, + encoder: format.NewEncoder(cryptor), + flavor: flavor, + helper: helper, + metronClient: metronClient, + debugStartActualLRPHeartbeats: debugStartActualLRPHeartbeats, + } +} + +func (db *SQLDB) transact(ctx context.Context, logger lager.Logger, f func(logger lager.Logger, tx helpers.Tx) error) error { + err := db.helper.Transact(ctx, logger, db.db, f) + if err != nil { + return db.convertSQLError(err) + } + return nil +} + +func (db *SQLDB) serializeModel(logger lager.Logger, model format.Model) ([]byte, error) { + encodedPayload, err := db.serializer.Marshal(logger, model) + if err != nil { + logger.Error("failed-to-serialize-model", err) + return nil, models.NewError(models.Error_InvalidRecord, err.Error()) + } + return encodedPayload, nil +} + +func (db *SQLDB) deserializeModel(logger lager.Logger, data []byte, model format.Model) error { + err := db.serializer.Unmarshal(logger, data, model) + if err != nil { + logger.Error("failed-to-deserialize-model", err) + return models.NewError(models.Error_InvalidRecord, err.Error()) + } + return nil +} + +func (db *SQLDB) convertSQLError(err error) *models.Error { + converted := db.helper.ConvertSQLError(err) + switch converted { + case helpers.ErrResourceExists: + return models.ErrResourceExists + case helpers.ErrDeadlock: + return models.ErrDeadlock + case helpers.ErrBadRequest: + return models.ErrBadRequest + case helpers.ErrUnrecoverableError: + return models.NewUnrecoverableError(err) + case helpers.ErrResourceNotFound: + return models.ErrResourceNotFound + default: + return models.ConvertError(err) + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/suspect_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/suspect_db.go new file mode 100644 index 0000000000..ef25e9bf94 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/suspect_db.go @@ -0,0 +1,100 @@ +package sqldb + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) RemoveSuspectActualLRP(ctx context.Context, logger lager.Logger, lrpKey *models.ActualLRPKey) (*models.ActualLRP, error) { + logger = logger.Session("db-remove-suspect-actual-lrp", lager.Data{"lrp_key": lrpKey}) + logger.Debug("starting") + defer logger.Debug("complete") + + var ( + lrp *models.ActualLRP + err error + ) + + err = db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + processGuid := lrpKey.ProcessGuid + index := lrpKey.Index + + lrp, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Suspect, tx) + if err == models.ErrResourceNotFound { + logger.Debug("suspect-lrp-does-not-exist") + return nil + } + + if err != nil { + logger.Error("failed-fetching-actual-lrp", err) + return err + } + + _, err = db.delete(ctx, logger, tx, "actual_lrps", + "process_guid = ? AND instance_index = ? AND presence = ?", + processGuid, index, models.ActualLRP_Suspect, + ) + + if err != nil { + logger.Error("failed-delete", err) + return models.ErrActualLRPCannotBeRemoved + } + + return nil + }) + + return lrp, err +} + +func (db *SQLDB) PromoteSuspectActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32) (*models.ActualLRP, *models.ActualLRP, *models.ActualLRP, error) { + logger = logger.Session("promote-suspect-actual-lrp", lager.Data{"process_guid": processGuid, "index": index}) + logger.Info("starting") + defer logger.Info("complete") + + var ( + beforeLRP *models.ActualLRP + afterLRP models.ActualLRP + ordinaryLRP *models.ActualLRP + ) + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + beforeLRP, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Suspect, tx) + if err != nil { + logger.Error("failed-fetching-suspect-actual-lrp", err) + return err + } + + ordinaryLRP, err = db.fetchActualLRPForUpdate(ctx, logger, processGuid, index, models.ActualLRP_Ordinary, tx) + if err != nil && err != models.ErrResourceNotFound { + logger.Error("failed-fetching-ordinary-actual-lrp", err) + return err + } + if err != models.ErrResourceNotFound { + _, err = db.delete(ctx, logger, tx, actualLRPsTable, + "process_guid = ? AND instance_index = ? AND presence = ?", + processGuid, index, models.ActualLRP_Ordinary, + ) + if err != nil { + logger.Error("failed-removing-ordinaryactual-lrp", err) + return err + } + } + + afterLRP = *beforeLRP + afterLRP.Presence = models.ActualLRP_Ordinary + wheres := "process_guid = ? AND instance_index = ? AND presence = ?" + _, err = db.update(ctx, logger, tx, actualLRPsTable, helpers.SQLAttributes{ + "presence": afterLRP.Presence, + }, wheres, processGuid, index, beforeLRP.Presence) + if err != nil { + logger.Error("failed-updating-lrp", err) + } + + return nil + }) + + return beforeLRP, &afterLRP, ordinaryLRP, err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_convergence.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_convergence.go new file mode 100644 index 0000000000..02afe59c26 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_convergence.go @@ -0,0 +1,403 @@ +package sqldb + +import ( + "context" + "fmt" + "math" + "strings" + "time" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const ( + expiredFailureReason = "not started within time limit" + cellDisappearedFailureReason = "cell disappeared before completion" +) + +func (sqldb *SQLDB) ConvergeTasks(ctx context.Context, logger lager.Logger, cellSet models.CellSet, kickTasksDuration, expirePendingTaskDuration, expireCompletedTaskDuration time.Duration) db.TaskConvergenceResult { + logger = logger.Session("db-converge-tasks") + logger.Info("starting") + defer logger.Info("complete") + + convergenceResult := db.TaskConvergenceResult{} + + // failedEvents is a list of tasks that have transitioned from the pending to the completed state (but expired and failed) + // failedFetches are tasks that failed to deserialize (invalid task def) + // rowsAffected are the number of pending tasks that have expired + failedEvents, failedFetches, rowsAffected := sqldb.failExpiredPendingTasks(ctx, logger, expirePendingTaskDuration) + convergenceResult.Events = append(convergenceResult.Events, failedEvents...) + convergenceResult.Metrics.TasksPruned += failedFetches + convergenceResult.Metrics.TasksKicked += uint64(rowsAffected) + + // tasksToAuction is a list of tasks in the pending state that have not expired and are being auctioned + tasksToAuction, failedFetches := sqldb.getTaskStartRequestsForKickablePendingTasks(ctx, logger, expirePendingTaskDuration) + convergenceResult.TasksToAuction = tasksToAuction + convergenceResult.Metrics.TasksPruned += failedFetches + convergenceResult.Metrics.TasksKicked += uint64(len(tasksToAuction)) + + // failedEvents is a list of tasks that have transitioned from the running to completed state (but cell dissappeared and failed) + // rowsAffected is the number of running tasks that have lost their cells + failedEvents, failedFetches, rowsAffected = sqldb.failTasksWithDisappearedCells(ctx, logger, cellSet) + convergenceResult.Events = append(convergenceResult.Events, failedEvents...) + convergenceResult.Metrics.TasksPruned += failedFetches + convergenceResult.Metrics.TasksKicked += uint64(rowsAffected) + + // do this first so that we now have "Completed" tasks before cleaning up + // or re-sending the completion callback + // demotedEvents is a list of tasks transitioning from resolving back to completed state (bc they exceeded kickTasksDuration) + demotedEvents, failedFetches := sqldb.demoteKickableResolvingTasks(ctx, logger, kickTasksDuration) + convergenceResult.Events = append(convergenceResult.Events, demotedEvents...) + convergenceResult.Metrics.TasksPruned += failedFetches + + // removedEvents is a list of tasks in the completed stated that have been deleted bc the time since they initially changed to completed exceeded expireCompleteTaskDuration + removedEvents, rowsAffected := sqldb.deleteExpiredCompletedTasks(ctx, logger, expireCompletedTaskDuration) + convergenceResult.Events = append(convergenceResult.Events, removedEvents...) + convergenceResult.Metrics.TasksPruned += uint64(rowsAffected) + + // tasksToComplete is a list of tasks in the complete state that have exceeded kickTasksDuration + tasksToComplete, failedFetches := sqldb.getKickableCompleteTasksForCompletion(ctx, logger, kickTasksDuration) + convergenceResult.TasksToComplete = tasksToComplete + convergenceResult.Metrics.TasksPruned += failedFetches + convergenceResult.Metrics.TasksKicked += uint64(len(tasksToComplete)) + + convergenceResult.Metrics.TasksPending, convergenceResult.Metrics.TasksRunning, convergenceResult.Metrics.TasksCompleted, convergenceResult.Metrics.TasksResolving = sqldb.getTaskCountByState(ctx, logger) + + return convergenceResult +} + +func (db *SQLDB) failExpiredPendingTasks(ctx context.Context, logger lager.Logger, expirePendingTaskDuration time.Duration) ([]models.Event, uint64, int64) { + logger = logger.Session("fail-expired-pending-tasks") + + now := db.clock.Now() + + rows, err := db.all(ctx, logger, db.db, tasksTable, + taskColumns, helpers.NoLockRow, + "state = ? AND created_at < ?", models.Task_Pending, now.Add(-expirePendingTaskDuration).UnixNano()) + if err != nil { + logger.Error("failed-query", err) + return nil, 0, 0 + } + defer rows.Close() + + tasks, validTaskGuids, invalidTasksCount, err := db.fetchTasks(ctx, logger, rows, db.db, false) + if err != nil { + logger.Error("failed-fetching-some-tasks", err) + } + + wheres := []string{"state = ?", "created_at < ?"} + bindings := []interface{}{models.Task_Pending, now.Add(-expirePendingTaskDuration).UnixNano()} + + if len(validTaskGuids) == 0 { + return nil, uint64(invalidTasksCount), 0 + } + + wheres = append(wheres, fmt.Sprintf("guid IN (%s)", helpers.QuestionMarks(len(validTaskGuids)))) + for _, guid := range validTaskGuids { + bindings = append(bindings, guid) + } + + result, err := db.update(ctx, logger, db.db, tasksTable, + helpers.SQLAttributes{ + "failed": true, + "failure_reason": expiredFailureReason, + "result": "", + "state": models.Task_Completed, + "first_completed_at": now.UnixNano(), + "updated_at": now.UnixNano(), + }, + strings.Join(wheres, " AND "), bindings...) + if err != nil { + logger.Error("failed-query", err) + return nil, uint64(invalidTasksCount), 0 + } + + var events []models.Event + for _, task := range tasks { + afterTask := *task + afterTask.Failed = true + afterTask.FailureReason = expiredFailureReason + afterTask.Result = "" + afterTask.State = models.Task_Completed + afterTask.FirstCompletedAt = now.UnixNano() + afterTask.UpdatedAt = now.UnixNano() + + events = append(events, models.NewTaskChangedEvent(task, &afterTask)) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + logger.Error("failed-rows-affected", err) + return events, uint64(invalidTasksCount), 0 + } + return events, uint64(invalidTasksCount), rowsAffected +} + +func (db *SQLDB) getTaskStartRequestsForKickablePendingTasks(ctx context.Context, logger lager.Logger, expirePendingTaskDuration time.Duration) ([]*auctioneer.TaskStartRequest, uint64) { + logger = logger.Session("get-task-start-requests-for-kickable-pending-tasks") + + rows, err := db.all(ctx, logger, db.db, tasksTable, + taskColumns, helpers.NoLockRow, + "state = ? AND created_at > ?", + models.Task_Pending, db.clock.Now().Add(-expirePendingTaskDuration).UnixNano(), + ) + + if err != nil { + logger.Error("failed-query", err) + return []*auctioneer.TaskStartRequest{}, math.MaxUint64 + } + + defer rows.Close() + + tasksToAuction := []*auctioneer.TaskStartRequest{} + tasks, _, invalidTasksCount, err := db.fetchTasks(ctx, logger, rows, db.db, false) + if err != nil { + logger.Error("failed-fetching-some-tasks", err) + } + + for _, task := range tasks { + taskStartRequest := auctioneer.NewTaskStartRequestFromModel(task.TaskGuid, task.Domain, task.TaskDefinition) + tasksToAuction = append(tasksToAuction, &taskStartRequest) + } + + return tasksToAuction, uint64(invalidTasksCount) +} + +func (db *SQLDB) failTasksWithDisappearedCells(ctx context.Context, logger lager.Logger, cellSet models.CellSet) ([]models.Event, uint64, int64) { + logger = logger.Session("fail-tasks-with-disappeared-cells") + + values := make([]interface{}, 0, 1+len(cellSet)) + values = append(values, models.Task_Running) + + for k := range cellSet { + values = append(values, k) + } + + wheres := "state = ?" + if len(cellSet) != 0 { + wheres += fmt.Sprintf(" AND cell_id NOT IN (%s)", helpers.QuestionMarks(len(cellSet))) + } + now := db.clock.Now().UnixNano() + + rows, err := db.all(ctx, logger, db.db, tasksTable, taskColumns, helpers.NoLockRow, wheres, values...) + if err != nil { + logger.Error("failed-query", err) + return nil, 0, 0 + } + defer rows.Close() + + tasks, validTaskGuids, invalidTasksCount, err := db.fetchTasks(ctx, logger, rows, db.db, false) + if err != nil { + logger.Error("failed-fetching-tasks", err) + } + + if len(validTaskGuids) == 0 { + return nil, uint64(invalidTasksCount), 0 + } + + wheres += fmt.Sprintf(" AND guid IN (%s)", helpers.QuestionMarks(len(validTaskGuids))) + + for _, guid := range validTaskGuids { + values = append(values, guid) + } + + result, err := db.update(ctx, logger, db.db, tasksTable, + helpers.SQLAttributes{ + "failed": true, + "failure_reason": cellDisappearedFailureReason, + "result": "", + "state": models.Task_Completed, + "first_completed_at": now, + "updated_at": now, + }, + wheres, values..., + ) + if err != nil { + logger.Error("failed-updating-tasks", err) + return nil, uint64(invalidTasksCount), 0 + } + + var events []models.Event + for _, task := range tasks { + afterTask := *task + afterTask.Failed = true + afterTask.FailureReason = cellDisappearedFailureReason + afterTask.Result = "" + afterTask.State = models.Task_Completed + afterTask.FirstCompletedAt = now + afterTask.UpdatedAt = now + + events = append(events, models.NewTaskChangedEvent(task, &afterTask)) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + logger.Error("failed-rows-affected", err) + return events, uint64(invalidTasksCount), 0 + } + + return events, uint64(invalidTasksCount), rowsAffected +} + +func (db *SQLDB) demoteKickableResolvingTasks(ctx context.Context, logger lager.Logger, kickTasksDuration time.Duration) ([]models.Event, uint64) { + logger = logger.Session("demote-kickable-resolving-tasks") + + rows, err := db.all(ctx, logger, db.db, tasksTable, + taskColumns, helpers.NoLockRow, + "state = ? AND updated_at < ?", models.Task_Resolving, db.clock.Now().Add(-kickTasksDuration).UnixNano(), + ) + if err != nil { + logger.Error("failed-query", err) + return nil, 0 + } + defer rows.Close() + + tasks, validTaskGuids, invalidTasksCount, err := db.fetchTasks(ctx, logger, rows, db.db, false) + if err != nil { + logger.Error("failed-fetching-tasks", err) + } + + wheres := []string{"state = ?", "updated_at < ?"} + bindings := []interface{}{models.Task_Resolving, db.clock.Now().Add(-kickTasksDuration).UnixNano()} + + if len(validTaskGuids) == 0 { + return nil, uint64(invalidTasksCount) + } + + wheres = append(wheres, fmt.Sprintf("guid IN (%s)", helpers.QuestionMarks(len(validTaskGuids)))) + + for _, guid := range validTaskGuids { + bindings = append(bindings, guid) + } + + _, err = db.update(ctx, logger, db.db, tasksTable, + helpers.SQLAttributes{"state": models.Task_Completed}, + strings.Join(wheres, " AND "), bindings..., + ) + if err != nil { + logger.Error("failed-updating-tasks", err) + } + + var events []models.Event + for _, task := range tasks { + afterTask := *task + afterTask.State = models.Task_Completed + events = append(events, models.NewTaskChangedEvent(task, &afterTask)) + } + + return events, uint64(invalidTasksCount) +} + +func (db *SQLDB) deleteExpiredCompletedTasks(ctx context.Context, logger lager.Logger, expireCompletedTaskDuration time.Duration) ([]models.Event, int64) { + logger = logger.Session("delete-expired-completed-tasks") + wheres := "state = ? AND first_completed_at < ?" + values := []interface{}{models.Task_Completed, db.clock.Now().Add(-expireCompletedTaskDuration).UnixNano()} + + rows, err := db.all(ctx, logger, db.db, tasksTable, + taskColumns, helpers.NoLockRow, + wheres, values..., + ) + if err != nil { + logger.Error("failed-query", err) + return nil, 0 + } + defer rows.Close() + + tasks, validTaskGuids, invalidTasksCount, err := db.fetchTasks(ctx, logger, rows, db.db, false) + if err != nil { + logger.Error("failed-fetching-tasks", err, lager.Data{"invalidTasksCound": int64(invalidTasksCount)}) + } + + if len(validTaskGuids) == 0 { + return nil, int64(invalidTasksCount) + } + + wheres += fmt.Sprintf(" AND guid IN (%s)", helpers.QuestionMarks(len(validTaskGuids))) + + for _, guid := range validTaskGuids { + values = append(values, guid) + } + + result, err := db.delete(ctx, logger, db.db, tasksTable, wheres, values...) + if err != nil { + logger.Error("failed-query", err) + return nil, int64(invalidTasksCount) + } + + var events []models.Event + for _, task := range tasks { + events = append(events, models.NewTaskRemovedEvent(task)) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + logger.Error("failed-rows-affected", err) + return events, int64(invalidTasksCount) + } + rowsAffected += int64(invalidTasksCount) + + return events, rowsAffected +} + +func (db *SQLDB) getKickableCompleteTasksForCompletion(ctx context.Context, logger lager.Logger, kickTasksDuration time.Duration) ([]*models.Task, uint64) { + logger = logger.Session("get-kickable-complete-tasks-for-completion") + + rows, err := db.all(ctx, logger, db.db, tasksTable, + taskColumns, helpers.NoLockRow, + "state = ? AND updated_at < ?", + models.Task_Completed, db.clock.Now().Add(-kickTasksDuration).UnixNano(), + ) + + if err != nil { + logger.Error("failed-query", err) + return []*models.Task{}, math.MaxUint64 + } + + defer rows.Close() + + tasksToComplete, _, failedFetches, err := db.fetchTasks(ctx, logger, rows, db.db, false) + + if err != nil { + logger.Error("failed-fetching-some-tasks", err) + } + + return tasksToComplete, uint64(failedFetches) +} + +func (db *SQLDB) getTaskCountByState(ctx context.Context, logger lager.Logger) (pendingCount, runningCount, completedCount, resolvingCount int) { + var query string + switch db.flavor { + case helpers.Postgres: + query = ` + SELECT + COUNT(*) FILTER (WHERE state = $1) AS pending_tasks, + COUNT(*) FILTER (WHERE state = $2) AS running_tasks, + COUNT(*) FILTER (WHERE state = $3) AS completed_tasks, + COUNT(*) FILTER (WHERE state = $4) AS resolving_tasks + FROM tasks + ` + case helpers.MySQL: + query = ` + SELECT + COUNT(IF(state = ?, 1, NULL)) AS pending_tasks, + COUNT(IF(state = ?, 1, NULL)) AS running_tasks, + COUNT(IF(state = ?, 1, NULL)) AS completed_tasks, + COUNT(IF(state = ?, 1, NULL)) AS resolving_tasks + FROM tasks + ` + default: + // totally shouldn't happen + panic("database flavor not implemented: " + db.flavor) + } + + row := db.db.QueryRowContext(ctx, query, models.Task_Pending, models.Task_Running, models.Task_Completed, models.Task_Resolving) + err := row.Scan(&pendingCount, &runningCount, &completedCount, &resolvingCount) + if err != nil { + logger.Error("failed-counting-tasks", err) + } + return +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_db.go new file mode 100644 index 0000000000..b58ebb548d --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/task_db.go @@ -0,0 +1,573 @@ +package sqldb + +import ( + "context" + "database/sql" + "strings" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) DesireTask(ctx context.Context, logger lager.Logger, taskDef *models.TaskDefinition, taskGuid, domain string) (*models.Task, error) { + logger = logger.Session("db-desire-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + + taskDefData, err := db.serializeModel(logger, taskDef) + if err != nil { + logger.Error("failed-serializing-task-definition", err) + return nil, err + } + + now := db.clock.Now().UnixNano() + err = db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + _, err = db.insert(ctx, logger, tx, tasksTable, + helpers.SQLAttributes{ + "guid": taskGuid, + "domain": domain, + "created_at": now, + "updated_at": now, + "first_completed_at": 0, + "state": models.Task_Pending, + "task_definition": taskDefData, + }, + ) + + return err + }) + + if err != nil { + logger.Error("failed-inserting-task", err) + return nil, err + } + + return &models.Task{ + TaskDefinition: taskDef, + TaskGuid: taskGuid, + Domain: domain, + CreatedAt: now, + UpdatedAt: now, + FirstCompletedAt: 0, + State: models.Task_Pending, + }, nil +} + +func (db *SQLDB) Tasks(ctx context.Context, logger lager.Logger, filter models.TaskFilter) ([]*models.Task, error) { + logger = logger.Session("db-tasks", lager.Data{"filter": filter}) + logger.Debug("starting") + defer logger.Debug("complete") + + wheres := []string{} + values := []interface{}{} + + if filter.Domain != "" { + wheres = append(wheres, "domain = ?") + values = append(values, filter.Domain) + } + + if filter.CellID != "" { + wheres = append(wheres, "cell_id = ?") + values = append(values, filter.CellID) + } + + results := []*models.Task{} + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + rows, err := db.all(ctx, logger, tx, tasksTable, + taskColumns, helpers.NoLockRow, + strings.Join(wheres, " AND "), values..., + ) + if err != nil { + logger.Error("failed-query", err) + return err + } + defer rows.Close() + + results, _, _, err = db.fetchTasks(ctx, logger, rows, tx, true) + if err != nil { + logger.Error("failed-fetch", err) + return err + } + + return nil + }) + + return results, err +} + +func (db *SQLDB) TaskByGuid(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, error) { + logger = logger.Session("db-task-by-guid", lager.Data{"task_guid": taskGuid}) + logger.Debug("starting") + defer logger.Debug("complete") + + var task *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + row := db.one(ctx, logger, tx, tasksTable, + taskColumns, helpers.NoLockRow, + "guid = ?", taskGuid, + ) + + task, err = db.fetchTask(ctx, logger, row, tx) + return err + }) + + return task, err +} + +func (db *SQLDB) StartTask(ctx context.Context, logger lager.Logger, taskGuid, cellId string) (*models.Task, *models.Task, bool, error) { + logger = logger.Session("db-start-task", lager.Data{"task_guid": taskGuid, "cell_id": cellId}) + logger.Info("starting") + defer logger.Info("complete") + + var started bool + var beforeTask models.Task + var afterTask *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + beforeTask = *afterTask + if afterTask.State == models.Task_Running && afterTask.CellId == cellId { + logger.Debug("task-already-running-on-cell") + return nil + } + + if err = afterTask.ValidateTransitionTo(models.Task_Running); err != nil { + logger.Error("failed-to-transition-task-to-running", err) + return err + } + + now := db.clock.Now().UnixNano() + _, err = db.update(ctx, logger, tx, tasksTable, + helpers.SQLAttributes{ + "state": models.Task_Running, + "updated_at": now, + "cell_id": cellId, + }, + "guid = ?", taskGuid, + ) + if err != nil { + return err + } + + afterTask.State = models.Task_Running + afterTask.UpdatedAt = now + afterTask.CellId = cellId + + started = true + return nil + }) + + return &beforeTask, afterTask, started, err +} + +func (db *SQLDB) CancelTask(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, *models.Task, string, error) { + logger = logger.Session("db-cancel-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeTask models.Task + var afterTask *models.Task + var cellID string + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + beforeTask = *afterTask + cellID = afterTask.CellId + + if err = afterTask.ValidateTransitionTo(models.Task_Completed); err != nil { + if afterTask.State != models.Task_Pending { + logger.Error("failed-to-transition-task-to-completed", err) + return err + } + } + err = db.completeTask(ctx, logger, afterTask, true, "task was cancelled", "", tx) + if err != nil { + return err + } + + return nil + }) + + return &beforeTask, afterTask, cellID, err +} + +func (db *SQLDB) CompleteTask(ctx context.Context, logger lager.Logger, taskGuid, cellID string, failed bool, failureReason, taskResult string) (*models.Task, *models.Task, error) { + logger = logger.Session("db-complete-task", lager.Data{"task_guid": taskGuid, "cell_id": cellID}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeTask models.Task + var afterTask *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + beforeTask = *afterTask + + if afterTask.CellId != cellID && afterTask.State == models.Task_Running { + logger.Error("failed-task-already-running-on-different-cell", err) + return models.NewRunningOnDifferentCellError(cellID, afterTask.CellId) + } + + if err = afterTask.ValidateTransitionTo(models.Task_Completed); err != nil { + logger.Error("failed-to-transition-task-to-completed", err) + return err + } + + err = db.completeTask(ctx, logger, afterTask, failed, failureReason, taskResult, tx) + if err != nil { + return err + } + + return nil + }) + + return &beforeTask, afterTask, err +} + +func (db *SQLDB) FailTask(ctx context.Context, logger lager.Logger, taskGuid, failureReason string) (*models.Task, *models.Task, error) { + logger = logger.Session("db-fail-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeTask models.Task + var afterTask *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + beforeTask = *afterTask + + if err = afterTask.ValidateTransitionTo(models.Task_Completed); err != nil { + if afterTask.State != models.Task_Pending { + logger.Error("failed-to-transition-task-to-completed", err) + return err + } + } + + err = db.completeTask(ctx, logger, afterTask, true, failureReason, "", tx) + if err != nil { + return err + } + + return nil + }) + + return &beforeTask, afterTask, err +} + +func (db *SQLDB) RejectTask(ctx context.Context, logger lager.Logger, taskGuid, rejectionReason string) (*models.Task, *models.Task, error) { + logger = logger.Session("db-reject-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + var beforeTask models.Task + var afterTask *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + if afterTask.State != models.Task_Pending && afterTask.State != models.Task_Running { + logger.Info("invalid-task-state", lager.Data{"task_state": afterTask.State}) + return models.ErrBadRequest + } + + beforeTask = *afterTask + + now := db.clock.Now().UnixNano() + + afterTask.RejectionCount++ + afterTask.RejectionReason = truncateString(rejectionReason, 1024) + afterTask.State = models.Task_Pending + afterTask.UpdatedAt = now + + _, err = db.update(ctx, logger, tx, tasksTable, + helpers.SQLAttributes{ + "rejection_count": afterTask.RejectionCount, + "rejection_reason": afterTask.RejectionReason, + "updated_at": afterTask.UpdatedAt, + "state": afterTask.State, + }, + "guid = ?", taskGuid, + ) + if err != nil { + logger.Error("failed-updating-tasks", err) + return err + } + + return nil + }) + + return &beforeTask, afterTask, err +} + +// The stager calls this when it wants to claim a completed task. This ensures that only one +// stager ever attempts to handle a completed task +func (db *SQLDB) ResolvingTask(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, *models.Task, error) { + logger = logger.Session("db-resolving-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + + var beforeTask models.Task + var afterTask *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + afterTask, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + beforeTask = *afterTask + + if err = afterTask.ValidateTransitionTo(models.Task_Resolving); err != nil { + logger.Error("invalid-state-transition", err) + return err + } + + now := db.clock.Now().UnixNano() + _, err = db.update(ctx, logger, tx, tasksTable, + helpers.SQLAttributes{ + "state": models.Task_Resolving, + "updated_at": now, + }, + "guid = ?", taskGuid, + ) + if err != nil { + logger.Error("failed-updating-tasks", err) + return err + } + + afterTask.State = models.Task_Resolving + afterTask.UpdatedAt = now + + return nil + }) + + return &beforeTask, afterTask, err +} + +func (db *SQLDB) DeleteTask(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, error) { + logger = logger.Session("db-delete-task", lager.Data{"task_guid": taskGuid}) + logger.Info("starting") + defer logger.Info("complete") + + var task *models.Task + + err := db.transact(ctx, logger, func(logger lager.Logger, tx helpers.Tx) error { + var err error + task, err = db.fetchTaskForUpdate(ctx, logger, taskGuid, tx) + if err != nil { + logger.Error("failed-locking-task", err) + return err + } + + if task.State != models.Task_Resolving { + err = models.NewTaskTransitionError(task.State, models.Task_Resolving) + logger.Error("invalid-state-transition", err) + return err + } + + _, err = db.delete(ctx, logger, tx, tasksTable, "guid = ?", taskGuid) + if err != nil { + logger.Error("failed-deleting-task", err) + return err + } + + return nil + }) + return task, err +} + +func (db *SQLDB) completeTask(ctx context.Context, logger lager.Logger, task *models.Task, failed bool, failureReason, result string, tx helpers.Tx) error { + now := db.clock.Now().UnixNano() + + task.State = models.Task_Completed + task.UpdatedAt = now + task.FirstCompletedAt = now + task.Failed = failed + task.FailureReason = truncateString(failureReason, 1024) + task.Result = result + task.CellId = "" + + _, err := db.update(ctx, logger, tx, tasksTable, + helpers.SQLAttributes{ + "failed": task.Failed, + "failure_reason": task.FailureReason, + "result": task.Result, + "state": task.State, + "first_completed_at": task.FirstCompletedAt, + "updated_at": task.UpdatedAt, + "cell_id": "", + }, + "guid = ?", task.TaskGuid, + ) + if err != nil { + logger.Error("failed-updating-tasks", err) + return err + } + + return nil +} + +func (db *SQLDB) fetchTaskForUpdate(ctx context.Context, logger lager.Logger, taskGuid string, queryable helpers.Queryable) (*models.Task, error) { + row := db.one(ctx, logger, queryable, tasksTable, + taskColumns, helpers.LockRow, + "guid = ?", taskGuid, + ) + return db.fetchTask(ctx, logger, row, queryable) +} + +func (db *SQLDB) fetchTasks(ctx context.Context, logger lager.Logger, rows *sql.Rows, queryable helpers.Queryable, abortOnError bool) ([]*models.Task, []string, int, error) { + tasks := []*models.Task{} + invalidGuids := []string{} + validGuids := []string{} + var err error + for rows.Next() { + var task *models.Task + var guid string + + task, guid, err = db.fetchTaskInternal(logger, rows) + if err != nil { + if err == models.ErrDeserialize { + invalidGuids = append(invalidGuids, guid) + } + + if abortOnError { + break + } + + continue + } + + tasks = append(tasks, task) + validGuids = append(validGuids, task.TaskGuid) + } + + if err == nil { + err = rows.Err() + } + + closeErr := rows.Close() + if closeErr != nil { + logger.Debug("failed-to-close-row", lager.Data{"error": closeErr}) + } + + if len(invalidGuids) > 0 { + deleteErr := db.deleteInvalidTasks(ctx, logger, queryable, invalidGuids...) + if deleteErr != nil { + logger.Error("failed-to-delete-invalid-task", err, lager.Data{"guids": invalidGuids}) + } + } + + return tasks, validGuids, len(invalidGuids), err +} + +func (db *SQLDB) fetchTask(ctx context.Context, logger lager.Logger, scanner helpers.RowScanner, queryable helpers.Queryable) (*models.Task, error) { + task, guid, err := db.fetchTaskInternal(logger, scanner) + if err == models.ErrDeserialize { + deleteErr := db.deleteInvalidTasks(ctx, logger, queryable, guid) + if deleteErr != nil { + logger.Error("failed-to-delete-invalid-task", err, lager.Data{"guid": guid}) + } + } + return task, err +} + +func (db *SQLDB) fetchTaskInternal(logger lager.Logger, scanner helpers.RowScanner) (*models.Task, string, error) { + var guid, domain, cellID, failureReason, rejectionReason string + var result sql.NullString + var createdAt, updatedAt, firstCompletedAt int64 + var state, rejectionCount int32 + var failed bool + var taskDefData []byte + + err := scanner.Scan( + &guid, + &domain, + &updatedAt, + &createdAt, + &firstCompletedAt, + &state, + &cellID, + &result, + &failed, + &failureReason, + &taskDefData, + &rejectionCount, + &rejectionReason, + ) + + if err == sql.ErrNoRows { + return nil, "", err + } + + if err != nil { + logger.Error("failed-scanning-row", err) + return nil, "", err + } + + var taskDef models.TaskDefinition + err = db.deserializeModel(logger, taskDefData, &taskDef) + if err != nil { + return nil, guid, models.ErrDeserialize + } + + task := &models.Task{ + TaskGuid: guid, + Domain: domain, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + FirstCompletedAt: firstCompletedAt, + State: models.Task_State(state), + CellId: cellID, + Result: result.String, + Failed: failed, + FailureReason: failureReason, + TaskDefinition: &taskDef, + RejectionCount: rejectionCount, + RejectionReason: rejectionReason, + } + return task, guid, nil +} + +func (db *SQLDB) deleteInvalidTasks(ctx context.Context, logger lager.Logger, queryable helpers.Queryable, guids ...string) error { + for _, guid := range guids { + logger.Info("deleting-invalid-task-from-db", lager.Data{"guid": guid}) + _, err := db.delete(ctx, logger, queryable, tasksTable, "guid = ?", guid) + if err != nil { + logger.Error("failed-deleting-task", err) + } + } + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/version_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/version_db.go new file mode 100644 index 0000000000..f2e765f538 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/sqldb/version_db.go @@ -0,0 +1,59 @@ +package sqldb + +import ( + "context" + "encoding/json" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +const VersionID = "version" + +func (db *SQLDB) SetVersion(tx helpers.Tx, ctx context.Context, logger lager.Logger, version *models.Version) error { + logger = logger.Session("db-set-version", lager.Data{"version": version}) + logger.Debug("starting") + defer logger.Debug("complete") + + versionJSON, err := json.Marshal(version) + if err != nil { + logger.Error("failed-marshalling-version", err) + return err + } + err = db.helper.RetryOnDeadlock(logger, func() error { + return db.setConfigurationValue(tx, ctx, logger, VersionID, string(versionJSON)) + }) + if err != nil { + return db.convertSQLError(err) + } + return nil +} + +func (db *SQLDB) Version(tx helpers.Tx, ctx context.Context, logger lager.Logger) (*models.Version, error) { + logger = logger.Session("db-version") + logger.Debug("starting") + defer logger.Debug("complete") + + var versionJSON string + + err := db.helper.RetryOnDeadlock(logger, func() error { + var err error + versionJSON, err = db.getConfigurationValue(tx, ctx, logger, VersionID) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, db.convertSQLError(err) + } + var version models.Version + err = json.Unmarshal([]byte(versionJSON), &version) + if err != nil { + logger.Error("failed-to-deserialize-version", err) + return nil, models.ErrDeserialize + } + + return &version, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/suspect_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/suspect_db.go new file mode 100644 index 0000000000..adb1cecf31 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/suspect_db.go @@ -0,0 +1,15 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . SuspectDB + +type SuspectDB interface { + RemoveSuspectActualLRP(context.Context, lager.Logger, *models.ActualLRPKey) (*models.ActualLRP, error) + PromoteSuspectActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32) (*models.ActualLRP, *models.ActualLRP, *models.ActualLRP, error) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/task_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/task_db.go new file mode 100644 index 0000000000..ac6a95f5dd --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/task_db.go @@ -0,0 +1,46 @@ +package db + +import ( + "context" + "time" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +type CompleteTaskWork func(logger lager.Logger, taskDB TaskDB, task *models.Task) func() + +type TaskConvergenceResult struct { + TasksToAuction []*auctioneer.TaskStartRequest + TasksToComplete []*models.Task + Events []models.Event + + Metrics TaskMetrics +} + +type TaskMetrics struct { + TasksPending int + TasksRunning int + TasksCompleted int + TasksResolving int + TasksPruned uint64 + TasksKicked uint64 +} + +//counterfeiter:generate . TaskDB +type TaskDB interface { + Tasks(ctx context.Context, logger lager.Logger, filter models.TaskFilter) ([]*models.Task, error) + TaskByGuid(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, error) + + DesireTask(ctx context.Context, logger lager.Logger, taskDefinition *models.TaskDefinition, taskGuid, domain string) (*models.Task, error) + StartTask(ctx context.Context, logger lager.Logger, taskGuid, cellId string) (before *models.Task, after *models.Task, shouldStart bool, rr error) + CancelTask(ctx context.Context, logger lager.Logger, taskGuid string) (before *models.Task, after *models.Task, cellID string, err error) + FailTask(ctx context.Context, logger lager.Logger, taskGuid, failureReason string) (before *models.Task, after *models.Task, err error) + RejectTask(ctx context.Context, logger lager.Logger, taskGuid, rejectionReason string) (before *models.Task, after *models.Task, err error) + CompleteTask(ctx context.Context, logger lager.Logger, taskGuid, cellId string, failed bool, failureReason, result string) (before *models.Task, after *models.Task, err error) + ResolvingTask(ctx context.Context, logger lager.Logger, taskGuid string) (before *models.Task, after *models.Task, err error) + DeleteTask(ctx context.Context, logger lager.Logger, taskGuid string) (task *models.Task, err error) + + ConvergeTasks(ctx context.Context, logger lager.Logger, cellSet models.CellSet, kickTaskDuration, expirePendingTaskDuration, expireCompletedTaskDuration time.Duration) TaskConvergenceResult +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/version_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/version_db.go new file mode 100644 index 0000000000..3c4a1125de --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/db/version_db.go @@ -0,0 +1,15 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate . VersionDB +type VersionDB interface { + Version(tx helpers.Tx, ctx context.Context, logger lager.Logger) (*models.Version, error) + SetVersion(tx helpers.Tx, ctx context.Context, logger lager.Logger, version *models.Version) error +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/encryptor.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/encryptor.go new file mode 100644 index 0000000000..d295ff02f5 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/encryptor.go @@ -0,0 +1,96 @@ +package encryptor + +import ( + "context" + "errors" + "os" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +const ( + encryptionDuration = "EncryptionDuration" +) + +type Encryptor struct { + logger lager.Logger + db db.EncryptionDB + keyManager encryption.KeyManager + cryptor encryption.Cryptor + clock clock.Clock + metronClient loggingclient.IngressClient +} + +func New( + logger lager.Logger, + db db.EncryptionDB, + keyManager encryption.KeyManager, + cryptor encryption.Cryptor, + clock clock.Clock, + metronClient loggingclient.IngressClient, +) Encryptor { + return Encryptor{ + logger: logger, + db: db, + keyManager: keyManager, + cryptor: cryptor, + clock: clock, + metronClient: metronClient, + } +} + +func (m Encryptor) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := m.logger.Session("encryptor") + logger.Info("starting") + defer logger.Info("exited") + + currentEncryptionKey, err := m.db.EncryptionKeyLabel(context.Background(), logger) + if err != nil { + if models.ConvertError(err) != models.ErrResourceNotFound { + logger.Error("failed-to-fetch-encryption-key-label", err) + return err + } + } else { + if m.keyManager.DecryptionKey(currentEncryptionKey) == nil { + err := errors.New("Existing encryption key version (" + currentEncryptionKey + ") is not among the known keys") + logger.Error("unknown-encryption-key-lable", err) + return err + } + } + + close(ready) + + if currentEncryptionKey != m.keyManager.EncryptionKey().Label() { + logger := logger.WithData(lager.Data{ + "desired-key-label": m.keyManager.EncryptionKey().Label(), + "existing-key-label": currentEncryptionKey, + }) + + encryptionStart := m.clock.Now() + logger.Info("encryption-started") + err := m.db.PerformEncryption(context.Background(), logger) + if err != nil { + logger.Error("encryption-failed", err) + } else { + err = m.db.SetEncryptionKeyLabel(context.Background(), logger, m.keyManager.EncryptionKey().Label()) + if err != nil { + return err + } + } + + totalTime := m.clock.Since(encryptionStart) + logger.Info("encryption-finished", lager.Data{"total_time": totalTime}) + err = m.metronClient.SendDuration(encryptionDuration, totalTime) + if err != nil { + logger.Error("failed-to-send-encryption-duration-metrics", err) + } + } + + <-signals + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/package.go new file mode 100644 index 0000000000..a53b788fd5 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/encryptor/package.go @@ -0,0 +1 @@ +package encryptor // import "code.cloudfoundry.org/bbs/encryptor" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/actual_lrp_event_calculator.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/actual_lrp_event_calculator.go new file mode 100644 index 0000000000..d77fbac9d6 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/actual_lrp_event_calculator.go @@ -0,0 +1,446 @@ +package calculator + +import ( + "sort" + + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/models" +) + +// LRP Instance Constraints: +// - an ActualLRP is identified by its ActualLRPKey + ActualLRPInstanceKey +// - as long as these don't change, any change in presence or state will result in a changed event +// - an ActualLRP transitioning from Unclaimed -> Claimed will result in a +// changed event (this is not exactly an exception since an ActualLRPInstanceKey +// doesn't have a defined identity by this constraint) +// - an ActualLRP cannot transition directly from having an +// ActualLRPInstanceKey to having none (i.e. Running -> Unclaimed). This must +// result in a removed and a created event. +// +// LRP Group emissions: +// - If the slot changes from nil to something, that's an ActualLRPCreated event +// - If the slot changes from something to nil, that's an ActualLRPRemoved event +// - If the Instance slot changes from one ActualLRPInstanceKey to another, +// it's considered an ActualLRPCreated and ActualLRPRemoved event +// - If the instance slot does not change the ActualLRPInstanceKey, it's +// considered an ActualLRPChanged event (including the transition from Running -> +// Unclaimed, e.g. when the LRP crashes) +// - If the instance slot changes from Unclaimed to another state, it's still considered a changed event +// +// General: +// - We should emit Crashed events followed by Create or Changed events where +// the resulting LRP is in the running state before Remove events +// +// Events that follow are instance events. LRP group events go through a separate algorithm. +// +// ClaimActualLRP +// Changes Allowed: +// - Unclaimed -> Claimed (Changed event) +// - Running -> Claimed (Changed event) +// - Crashed -> Claimed (Changed event) +// - Claimed -> Claimed (No event) +// +// Transition is only allowed if LRPInstanceKey will be the same or if the LRP +// is Unclaimed. No case this will result in any other event. +// +// UnclaimActualLRP +// Changes Allowed: +// Not Unclaimed -> Unclaimed (Should emit removed and created event) +// +// StartActualLRP +// Changes Allowed: +// nil -> Running (Created event) +// Unclaimed -> Running (if instanceKey matches) (changed event) +// Claimed -> Running (if instanceKey matches) (changed event) +// Running -> Running (if instanceKey matches) (changed event) (Only allowed if netInfo has changed) +// +// if the lrp being started is suspect: +// do nothing +// if suspect exists and it is not the lrp being started: +// emit removed event for the suspect LRP +// +// CrashActualLRP +// Changes Allowed: +// Claimed -> Crashed (if instanceKey matches) (changed event) +// Running -> Crashed (if instanceKey matches) (changed event) +// +// Claimed -> Unclaimed (if instanceKey matches) (if crashedCount is below) (created + removed event) +// Running -> Unclaimed (if instanceKey matches) (if crashCount is below) (created + removed event) +// +// FailActualLRP +// Unclaimed -> Unclaimed (no events emitted) +// +// RemoveActualLRP +// removed event + +type ActualLRPEventCalculator struct { + // Deprecated: use ActualLRPInstanceHub instead + ActualLRPGroupHub events.Hub + ActualLRPInstanceHub events.Hub +} + +// EmitCrashEvents emits only the events for a crash scenario. Specifically: +// - A CrashEvent each for the Group Hub and the Instance Hub +// - A ChangedEvent for the Group Hub +// - Either a ChangedEvent or a Removed and Created Event for the Instance Hub, depending on the state +// of the ActualLRP +// +// This function was added to work around a bug in the existing logic for EmitEvents, where CrashedEvents +// were not being emitted due to the CrashResetTimeout +func (e ActualLRPEventCalculator) EmitCrashEvents(traceId string, beforeSet, afterSet []*models.ActualLRP) { + beforeGroup := models.ResolveActualLRPGroup(beforeSet) + afterGroup := models.ResolveActualLRPGroup(removeNilLRPs(afterSet)) + + groupEvents := []models.Event{} + if !beforeGroup.Instance.Equal(afterGroup.Instance) { + if afterGroup.Instance == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + groupEvents = append(groupEvents, models.NewActualLRPRemovedEvent(beforeGroup.Instance.ToActualLRPGroup())) + } else if beforeGroup.Instance == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + groupEvents = append(groupEvents, models.NewActualLRPCreatedEvent(afterGroup.Instance.ToActualLRPGroup())) + } else { + groupEvents = append(groupEvents, + models.NewActualLRPCrashedEvent(beforeGroup.Instance, afterGroup.Instance), + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPChangedEvent(beforeGroup, afterGroup), + ) + } + } + + if !beforeGroup.Evacuating.Equal(afterGroup.Evacuating) { + if afterGroup.Evacuating == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + groupEvents = append(groupEvents, models.NewActualLRPRemovedEvent(beforeGroup.Evacuating.ToActualLRPGroup())) + } else if beforeGroup.Evacuating == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + groupEvents = append(groupEvents, models.NewActualLRPCreatedEvent(afterGroup.Evacuating.ToActualLRPGroup())) + } else { + groupEvents = append(groupEvents, + models.NewActualLRPCrashedEvent(beforeGroup.Evacuating, afterGroup.Evacuating), + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPChangedEvent(beforeGroup, afterGroup), + ) + } + } + + sort.Slice(groupEvents, func(i, j int) bool { + return EventScore(groupEvents[i]) > EventScore(groupEvents[j]) + }) + + for _, ev := range groupEvents { + e.ActualLRPGroupHub.Emit(ev) + } + + instanceEvents := []models.Event{} + for i := range afterSet { + before := beforeSet[i] + after := afterSet[i] + + if before.Equal(after) { + continue + } + + if after == nil { + instanceEvents = append(instanceEvents, + models.NewActualLRPInstanceRemovedEvent(before, traceId), + ) + } else if before == nil { + instanceEvents = append(instanceEvents, + models.NewActualLRPInstanceCreatedEvent(after, traceId), + ) + } else if after.State == models.ActualLRPStateCrashed { + instanceEvents = append(instanceEvents, + models.NewActualLRPCrashedEvent(before, after), + models.NewActualLRPInstanceChangedEvent(before, after, traceId), + ) + } else { + instanceEvents = append(instanceEvents, + models.NewActualLRPCrashedEvent(before, after), + models.NewActualLRPInstanceCreatedEvent(after, traceId), + models.NewActualLRPInstanceRemovedEvent(before, traceId), + ) + } + } + + sort.Slice(instanceEvents, func(i, j int) bool { + return EventScore(instanceEvents[i]) > EventScore(instanceEvents[j]) + }) + + for _, ev := range instanceEvents { + e.ActualLRPInstanceHub.Emit(ev) + } +} + +// EmitEvents emits the events such as when the changes identified in the +// events are applied to the beforeSet the resulting state is equal to +// afterSet. The beforeSet and afterSet are assumed to have the same process +// guid and index. +func (e ActualLRPEventCalculator) EmitEvents(traceId string, beforeSet, afterSet []*models.ActualLRP) { + events := []models.Event{} + + beforeGroup := models.ResolveActualLRPGroup(beforeSet) + afterGroup := models.ResolveActualLRPGroup(removeNilLRPs(afterSet)) + + for _, ev := range generateLRPGroupEvents(beforeGroup, afterGroup) { + e.ActualLRPGroupHub.Emit(ev) + } + + // stretch the two slices to be of equal size. make sure we do this after + // emitting the group events, otherwise ResolveActualLRPGroup will panic if + // it encounters nil lrps. + stretchSlice(&beforeSet, &afterSet) + + for i := range afterSet { + events = append(events, generateLRPInstanceEvents(beforeSet[i], afterSet[i], traceId)...) + } + + sort.Slice(events, func(i, j int) bool { + return EventScore(events[i]) > EventScore(events[j]) + }) + + for _, ev := range events { + e.ActualLRPInstanceHub.Emit(ev) + } +} + +// RecordChange returns a new LRP set with the before LRP replaced with after +// LRP. The index of after and before is the same. New LRPs (i.e. when before +// is nil) are appended to the end of the lrp slice. +func (e ActualLRPEventCalculator) RecordChange(before, after *models.ActualLRP, lrps []*models.ActualLRP) []*models.ActualLRP { + found := false + newLRPs := []*models.ActualLRP{} + for _, l := range lrps { + if l == nil { + // this entry is recording a LRP removal, just skip it + newLRPs = append(newLRPs, nil) + continue + } + + if before != nil && l.ActualLRPInstanceKey.Equal(before.ActualLRPInstanceKey) { + newLRPs = append(newLRPs, after) + found = true + } else { + newLRPs = append(newLRPs, l) + } + } + + if !found { + newLRPs = append(newLRPs, after) + } + + return newLRPs +} + +func generateCrashedInstanceEvents(before, after *models.ActualLRP, traceId string) []models.Event { + return wrapEvent( + models.NewActualLRPCrashedEvent(before, after), + models.NewActualLRPInstanceChangedEvent(before, after, traceId), + ) +} + +func generateUpdateInstanceEvents(before, after *models.ActualLRP, traceId string) []models.Event { + return wrapEvent( + models.NewActualLRPInstanceChangedEvent(before, after, traceId), + ) +} + +func generateUnclaimedInstanceEvents(before, after *models.ActualLRP, traceId string) []models.Event { + events := []models.Event{} + + // we can get here if auctioneer calls FailActualLRP + if before.State == models.ActualLRPStateUnclaimed { + return append(events, models.NewActualLRPInstanceChangedEvent(before, after, traceId)) + } + + return append( + events, + models.NewActualLRPInstanceCreatedEvent(after, traceId), + models.NewActualLRPInstanceRemovedEvent(before, traceId), + ) +} + +func generateLRPInstanceEvents(before, after *models.ActualLRP, traceId string) []models.Event { + if before.Equal(after) { + // nothing changed + return nil + } + + if after == nil { + return wrapEvent(models.NewActualLRPInstanceRemovedEvent(before, traceId)) + } + + if before == nil { + return wrapEvent(models.NewActualLRPInstanceCreatedEvent(after, traceId)) + } + + switch after.State { + case models.ActualLRPStateUnclaimed: + return generateUnclaimedInstanceEvents(before, after, traceId) + case models.ActualLRPStateClaimed: + return generateUpdateInstanceEvents(before, after, traceId) + case models.ActualLRPStateRunning: + return generateUpdateInstanceEvents(before, after, traceId) + case models.ActualLRPStateCrashed: + return generateCrashedInstanceEvents(before, after, traceId) + default: + return nil + } +} + +func generateCrashedGroupEvents(before, after *models.ActualLRP) []models.Event { + return wrapEvent( + models.NewActualLRPCrashedEvent(before, after), + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPChangedEvent(before.ToActualLRPGroup(), after.ToActualLRPGroup()), + ) +} + +func generateUnclaimedGroupEvents(before, after *models.ActualLRP) []models.Event { + events := []models.Event{} + + return append( + events, + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPChangedEvent(before.ToActualLRPGroup(), after.ToActualLRPGroup()), + ) +} + +func generateUpdateGroupEvents(before, after *models.ActualLRP) []models.Event { + if !before.ActualLRPInstanceKey.Empty() && + !after.ActualLRPInstanceKey.Equal(before.ActualLRPInstanceKey) { + // an Ordinary LRP replaced Suspect LRP + return wrapEvent( + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPCreatedEvent(after.ToActualLRPGroup()), + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + models.NewActualLRPRemovedEvent(before.ToActualLRPGroup()), + ) + } + + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + return wrapEvent(models.NewActualLRPChangedEvent(before.ToActualLRPGroup(), after.ToActualLRPGroup())) +} + +// The main difference between this function and generateLRPInstanceEvents +// (besides using different event types) is that the latter generates a +// remove+create events when the LRP is unclaimed. This function return a +// ActualLRPChangedEvent instead to be compatible with old subscribers. +func generateLRPInstanceGroupEvents(before, after *models.ActualLRP) []models.Event { + if before.Equal(after) { + // nothing changed + return nil + } + + if after == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + return wrapEvent(models.NewActualLRPRemovedEvent(before.ToActualLRPGroup())) + } + + if before == nil { + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + return wrapEvent(models.NewActualLRPCreatedEvent(after.ToActualLRPGroup())) + } + + switch after.State { + case models.ActualLRPStateUnclaimed: + return generateUnclaimedGroupEvents(before, after) + case models.ActualLRPStateClaimed: + return generateUpdateGroupEvents(before, after) + case models.ActualLRPStateRunning: + return generateUpdateGroupEvents(before, after) + case models.ActualLRPStateCrashed: + return generateCrashedGroupEvents(before, after) + default: + return nil + } +} + +// return the resulting lrp of the given event, that is the lrp being created +// or the lrp in the new lrp in a ActualLRPChanged event. Returns nil for +// crashed and removed events. Returns true iff this is a crashed event. +func getEventLRP(e models.Event) (*models.ActualLRP, bool) { + switch x := e.(type) { + //lint:ignore SA1019 - need to support this event until the deprecation becomes deletion + case *models.ActualLRPCreatedEvent: + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + lrp, _, _ := x.ActualLrpGroup.Resolve() + return lrp, false + //lint:ignore SA1019 - need to support this event until the deprecation becomes deletion + case *models.ActualLRPChangedEvent: + //lint:ignore SA1019 - still need to emit these events until the ActaulLRPGroup api is deleted + lrp, _, _ := x.After.Resolve() + return lrp, false + case *models.ActualLRPInstanceCreatedEvent: + return x.ActualLrp, false + case *models.ActualLRPInstanceChangedEvent: + return x.After.ToActualLRP(x.ActualLRPKey, x.ActualLRPInstanceKey), false + case *models.ActualLRPCrashedEvent: + return nil, true + } + + return nil, false +} + +// Determine the score of an event. An event with higher score should be +// emitted before lower ones. The score based ordering ensures continuous +// routability, so events with running instances should be emitted first +// followed by remove events. +func EventScore(e models.Event) int { + lrp, crashed := getEventLRP(e) + + // sort crashed events first to be backward compatible with the old + // event stream which emitted the crashed event before the + // remove/changed events. + if crashed { + return 2 + } + + // this is an event with a running instance, this should be emitted before + // any other event, such as removed ro changed event to non-running state. + if lrp != nil && lrp.State == models.ActualLRPStateRunning { + return 1 + } + + // The event is either a RemovedEvent or a ChangedEvent (to a non-RUNNING + // state). These are prioritized last, because those events cause loss of + // routability. + return 0 +} + +//lint:ignore SA1019 - deprecated model used in support of deprecated code until it gets deleted +func generateLRPGroupEvents(before, after *models.ActualLRPGroup) []models.Event { + events := generateLRPInstanceGroupEvents(before.Instance, after.Instance) + events = append(events, generateLRPInstanceGroupEvents(before.Evacuating, after.Evacuating)...) + + sort.Slice(events, func(i, j int) bool { + return EventScore(events[i]) > EventScore(events[j]) + }) + + return events +} + +// A Helper function to remove null lrps that could be added to the set if an +// LRP is removed. +func removeNilLRPs(lrps []*models.ActualLRP) []*models.ActualLRP { + newLRPs := []*models.ActualLRP{} + for _, l := range lrps { + if l == nil { + continue + } + newLRPs = append(newLRPs, l) + } + return newLRPs +} + +func stretchSlice(before, after *[]*models.ActualLRP) { + if len(*before) < len(*after) { + newLRPs := make([]*models.ActualLRP, len(*after)) + copy(newLRPs, *before) + *before = newLRPs + } +} + +func wrapEvent(e ...models.Event) []models.Event { + return e +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/package.go new file mode 100644 index 0000000000..a7f614e8b0 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/events/calculator/package.go @@ -0,0 +1 @@ +package calculator // import "code.cloudfoundry.org/bbs/events/calculator" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_handlers.go new file mode 100644 index 0000000000..51c089bdc4 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_handlers.go @@ -0,0 +1,146 @@ +package handlers + +import ( + "net/http" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +type ActualLRPHandler struct { + db db.ActualLRPDB + exitChan chan<- struct{} +} + +func NewActualLRPHandler(db db.ActualLRPDB, exitChan chan<- struct{}) *ActualLRPHandler { + return &ActualLRPHandler{ + db: db, + exitChan: exitChan, + } +} + +func (h *ActualLRPHandler) ActualLRPs(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("actual-lrps").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.ActualLRPsRequest{} + response := &models.ActualLRPsResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + var index *int32 + if request.IndexExists() { + i := request.GetIndex() + index = &i + } + filter := models.ActualLRPFilter{Domain: request.Domain, CellID: request.CellId, Index: index, ProcessGuid: request.ProcessGuid} + response.ActualLrps, err = h.db.ActualLRPs(req.Context(), logger, filter) + } + + response.Error = models.ConvertError(err) + + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +func (h *ActualLRPHandler) ActualLRPsByProcessGuids(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("multiple-actual-lrps").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.ActualLRPsByProcessGuidsRequest{} + response := &models.ActualLRPsByProcessGuidsResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + filter := models.ActualLRPsByProcessGuidsFilter{ProcessGuids: request.ProcessGuids} + response.ActualLrps, err = h.db.ActualLRPsByProcessGuids(req.Context(), logger, filter) + } + + response.Error = models.ConvertError(err) + + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +// Deprecated: use ActaulLRPs instead +func (h *ActualLRPHandler) ActualLRPGroups(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("actual-lrp-groups").WithTraceInfo(req) + + request := &models.ActualLRPGroupsRequest{} + response := &models.ActualLRPGroupsResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + filter := models.ActualLRPFilter{Domain: request.Domain, CellID: request.CellId} + lrps, err := h.db.ActualLRPs(req.Context(), logger, filter) + if err != nil { + response.Error = models.ConvertError(err) + return + } + response.ActualLrpGroups = models.ResolveActualLRPGroups(lrps) +} + +// Deprecated: use ActaulLRPs instead +func (h *ActualLRPHandler) ActualLRPGroupsByProcessGuid(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("actual-lrp-groups-by-process-guid").WithTraceInfo(req) + + request := &models.ActualLRPGroupsByProcessGuidRequest{} + response := &models.ActualLRPGroupsResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + filter := models.ActualLRPFilter{ProcessGuid: request.ProcessGuid} + lrps, err := h.db.ActualLRPs(req.Context(), logger, filter) + if err != nil { + response.Error = models.ConvertError(err) + return + } + response.ActualLrpGroups = models.ResolveActualLRPGroups(lrps) +} + +// Deprecated: use ActaulLRPs instead +func (h *ActualLRPHandler) ActualLRPGroupByProcessGuidAndIndex(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("actual-lrp-group-by-process-guid-and-index").WithTraceInfo(req) + + request := &models.ActualLRPGroupByProcessGuidAndIndexRequest{} + response := &models.ActualLRPGroupResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + filter := models.ActualLRPFilter{ProcessGuid: request.ProcessGuid, Index: &request.Index} + lrps, err := h.db.ActualLRPs(req.Context(), logger, filter) + + if err == nil && len(lrps) == 0 { + err = models.ErrResourceNotFound + } + + if err != nil { + response.Error = models.ConvertError(err) + return + } + response.ActualLrpGroup = models.ResolveActualLRPGroup(lrps) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_lifecycle_handler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_lifecycle_handler.go new file mode 100644 index 0000000000..f8d3defbf7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/actual_lrp_lifecycle_handler.go @@ -0,0 +1,207 @@ +package handlers + +import ( + "context" + "net/http" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/lager/v3" +) + +//go:generate counterfeiter -generate + +//counterfeiter:generate -o fake_controllers/fake_actual_lrp_lifecycle_controller.go . ActualLRPLifecycleController +type ActualLRPLifecycleController interface { + ClaimActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, actualLRPInstanceKey *models.ActualLRPInstanceKey) error + StartActualLRP(ctx context.Context, + logger lager.Logger, + actualLRPKey *models.ActualLRPKey, + actualLRPInstanceKey *models.ActualLRPInstanceKey, + actualLRPNetInfo *models.ActualLRPNetInfo, + actualLRPInternalRoutes []*models.ActualLRPInternalRoute, + actualLRPMetricTags map[string]string, + routable bool, + availabilityZone string, + ) error + CrashActualLRP(ctx context.Context, logger lager.Logger, actualLRPKey *models.ActualLRPKey, actualLRPInstanceKey *models.ActualLRPInstanceKey, errorMessage string) error + FailActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey, errorMessage string) error + RemoveActualLRP(ctx context.Context, logger lager.Logger, processGuid string, index int32, instanceKey *models.ActualLRPInstanceKey) error + RetireActualLRP(ctx context.Context, logger lager.Logger, key *models.ActualLRPKey) error +} + +type ActualLRPLifecycleHandler struct { + controller ActualLRPLifecycleController + exitChan chan<- struct{} +} + +func NewActualLRPLifecycleHandler( + controller ActualLRPLifecycleController, + exitChan chan<- struct{}, +) *ActualLRPLifecycleHandler { + return &ActualLRPLifecycleHandler{ + controller: controller, + exitChan: exitChan, + } +} + +func (h *ActualLRPLifecycleHandler) ClaimActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("claim-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.ClaimActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.controller.ClaimActualLRP(req.Context(), logger, request.ProcessGuid, request.Index, request.ActualLrpInstanceKey) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) StartActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("start-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.StartActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + routable := true + if request.RoutableExists() { + r := request.GetRoutable() + routable = r + } + + err = h.controller.StartActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey, request.ActualLrpNetInfo, request.ActualLrpInternalRoutes, request.MetricTags, routable, request.AvailabilityZone) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) StartActualLRP_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("start-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.StartActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + routable := true + if request.RoutableExists() { + r := request.GetRoutable() + routable = r + } + + err = h.controller.StartActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey, request.ActualLrpNetInfo, []*models.ActualLRPInternalRoute{}, nil, routable, request.AvailabilityZone) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) CrashActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("crash-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.CrashActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + actualLRPKey := request.ActualLrpKey + actualLRPInstanceKey := request.ActualLrpInstanceKey + + err = h.controller.CrashActualLRP(req.Context(), logger, actualLRPKey, actualLRPInstanceKey, request.ErrorMessage) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) FailActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("fail-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.FailActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.controller.FailActualLRP(req.Context(), logger, request.ActualLrpKey, request.ErrorMessage) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) RemoveActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("remove-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.RemoveActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.controller.RemoveActualLRP(req.Context(), logger, request.ProcessGuid, request.Index, request.ActualLrpInstanceKey) + response.Error = models.ConvertError(err) +} + +func (h *ActualLRPLifecycleHandler) RetireActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("retire-actual-lrp").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + request := &models.RetireActualLRPRequest{} + response := &models.ActualLRPLifecycleResponse{} + + var err error + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.controller.RetireActualLRP(trace.ContextWithRequestId(req), logger, request.ActualLrpKey) + response.Error = models.ConvertError(err) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/cell_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/cell_handlers.go new file mode 100644 index 0000000000..df2c13dd2d --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/cell_handlers.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "net/http" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/lager/v3" +) + +type CellHandler struct { + serviceClient serviceclient.ServiceClient + exitChan chan<- struct{} +} + +func NewCellHandler(serviceClient serviceclient.ServiceClient, exitChan chan<- struct{}) *CellHandler { + return &CellHandler{ + serviceClient: serviceClient, + exitChan: exitChan, + } +} + +func (h *CellHandler) Cells(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("cells").WithTraceInfo(req) + response := &models.CellsResponse{} + cellSet, err := h.serviceClient.Cells(logger) + cells := []*models.CellPresence{} + for _, cp := range cellSet { + cells = append(cells, cp) + } + response.Cells = cells + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/desired_lrp_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/desired_lrp_handlers.go new file mode 100644 index 0000000000..ae4f5e37b5 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/desired_lrp_handlers.go @@ -0,0 +1,535 @@ +package handlers + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/events/calculator" + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/trace" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/rep" + "code.cloudfoundry.org/workpool" +) + +const BbsLogSource = "DIEGO-API" + +type DesiredLRPHandler struct { + desiredLRPDB db.DesiredLRPDB + actualLRPDB db.ActualLRPDB + desiredHub events.Hub + actualHub events.Hub + actualLRPInstanceHub events.Hub + auctioneerClient auctioneer.Client + repClientFactory rep.ClientFactory + serviceClient serviceclient.ServiceClient + updateWorkersCount int + exitChan chan<- struct{} + metronClient loggingclient.IngressClient +} + +func NewDesiredLRPHandler( + updateWorkersCount int, + desiredLRPDB db.DesiredLRPDB, + actualLRPDB db.ActualLRPDB, + desiredHub events.Hub, + actualHub events.Hub, + actualLRPInstanceHub events.Hub, + auctioneerClient auctioneer.Client, + repClientFactory rep.ClientFactory, + serviceClient serviceclient.ServiceClient, + exitChan chan<- struct{}, + metronClient loggingclient.IngressClient, +) *DesiredLRPHandler { + return &DesiredLRPHandler{ + desiredLRPDB: desiredLRPDB, + actualLRPDB: actualLRPDB, + desiredHub: desiredHub, + actualHub: actualHub, + actualLRPInstanceHub: actualLRPInstanceHub, + auctioneerClient: auctioneerClient, + repClientFactory: repClientFactory, + serviceClient: serviceClient, + updateWorkersCount: updateWorkersCount, + exitChan: exitChan, + metronClient: metronClient, + } +} + +func (h *DesiredLRPHandler) commonDesiredLRPs(logger lager.Logger, targetVersion format.Version, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desired-lrps").WithTraceInfo(req) + + request := &models.DesiredLRPsRequest{} + response := &models.DesiredLRPsResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + filter := models.DesiredLRPFilter{Domain: request.Domain, ProcessGuids: request.ProcessGuids, AppGuids: request.AppGuids} + + var desiredLRPs []*models.DesiredLRP + desiredLRPs, err = h.desiredLRPDB.DesiredLRPs(req.Context(), logger, filter) + for i, d := range desiredLRPs { + desiredLRPs[i] = d.VersionDownTo(targetVersion).PopulateMetricsGuid() + if len(desiredLRPs[i].CachedDependencies) == 0 { + desiredLRPs[i].CachedDependencies = nil + } + } + + response.DesiredLrps = desiredLRPs + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) + +} + +func (h *DesiredLRPHandler) DesiredLRPs(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonDesiredLRPs(logger, format.V3, w, req) +} + +func (h *DesiredLRPHandler) DesiredLRPs_r2(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonDesiredLRPs(logger, format.V2, w, req) +} + +func (h *DesiredLRPHandler) commonDesiredLRPByProcessGuid(logger lager.Logger, targetVersion format.Version, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desired-lrp-by-process-guid").WithTraceInfo(req) + + request := &models.DesiredLRPByProcessGuidRequest{} + response := &models.DesiredLRPResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + var desiredLRP *models.DesiredLRP + desiredLRP, err = h.desiredLRPDB.DesiredLRPByProcessGuid(req.Context(), logger, request.ProcessGuid) + if desiredLRP != nil { + desiredLRP = desiredLRP.VersionDownTo(targetVersion).PopulateMetricsGuid() + } + response.DesiredLrp = desiredLRP + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) + +} + +func (h *DesiredLRPHandler) DesiredLRPByProcessGuid(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonDesiredLRPByProcessGuid(logger, format.V3, w, req) +} + +func (h *DesiredLRPHandler) DesiredLRPByProcessGuid_r2(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonDesiredLRPByProcessGuid(logger, format.V2, w, req) +} + +func (h *DesiredLRPHandler) DesiredLRPSchedulingInfos(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desired-lrp-scheduling-infos").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.DesiredLRPsRequest{} + response := &models.DesiredLRPSchedulingInfosResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + filter := models.DesiredLRPFilter{ + Domain: request.Domain, + ProcessGuids: request.ProcessGuids, + AppGuids: request.AppGuids, + } + response.DesiredLrpSchedulingInfos, err = h.desiredLRPDB.DesiredLRPSchedulingInfos(req.Context(), logger, filter) + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +func (h *DesiredLRPHandler) DesiredLRPSchedulingInfoByProcessGuid(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desired-lrp-scheduling-info-by-process-guid").WithTraceInfo(req) + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.DesiredLRPByProcessGuidRequest{} + response := &models.DesiredLRPSchedulingInfoByProcessGuidResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + response.DesiredLrpSchedulingInfo, err = h.desiredLRPDB.DesiredLRPSchedulingInfoByProcessGuid(req.Context(), logger, request.ProcessGuid) + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +func (h *DesiredLRPHandler) DesiredLRPRoutingInfos(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desired-lrp-routing-infos") + logger.Debug("starting") + defer logger.Debug("complete") + + request := &models.DesiredLRPsRequest{} + response := &models.DesiredLRPsResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + filter := models.DesiredLRPFilter{ + Domain: request.Domain, + ProcessGuids: request.ProcessGuids, + } + response.DesiredLrps, err = h.desiredLRPDB.DesiredLRPRoutingInfos(req.Context(), logger, filter) + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +func (h *DesiredLRPHandler) DesireDesiredLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("desire-lrp").WithTraceInfo(req) + + request := &models.DesireLRPRequest{} + response := &models.DesiredLRPLifecycleResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + if err = h.logDesiredLrpParsingErrors(response.Error, request.GetDesiredLrp().GetProcessGuid()); err != nil { + logger.Error("failed-sending-app-logs", err) + } + return + } + + err = h.desiredLRPDB.DesireLRP(req.Context(), logger, request.DesiredLrp) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + desiredLRP, err := h.desiredLRPDB.DesiredLRPByProcessGuid(req.Context(), logger, request.DesiredLrp.ProcessGuid) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + go h.desiredHub.Emit(models.NewDesiredLRPCreatedEvent(desiredLRP, trace.RequestIdFromRequest(req))) + + schedulingInfo := request.DesiredLrp.DesiredLRPSchedulingInfo() + if schedulingInfo.Instances > 0 { + h.startInstanceRange(trace.ContextWithRequestId(req), logger, 0, schedulingInfo.Instances, &schedulingInfo) + } +} + +func (h *DesiredLRPHandler) UpdateDesiredLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("update-desired-lrp").WithTraceInfo(req) + + request := &models.UpdateDesiredLRPRequest{} + response := &models.DesiredLRPLifecycleResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + if err = h.logDesiredLrpParsingErrors(response.Error, request.GetProcessGuid()); err != nil { + logger.Error("failed-sending-app-logs", err) + } + return + } + + logger = logger.WithData(lager.Data{"guid": request.ProcessGuid}) + + logger.Debug("updating-desired-lrp") + beforeDesiredLRP, err := h.desiredLRPDB.UpdateDesiredLRP(req.Context(), logger, request.ProcessGuid, request.Update) + if err != nil { + logger.Debug("failed-updating-desired-lrp") + response.Error = models.ConvertError(err) + return + } + logger.Debug("completed-updating-desired-lrp") + + desiredLRP, err := h.desiredLRPDB.DesiredLRPByProcessGuid(req.Context(), logger, request.ProcessGuid) + if err != nil { + logger.Error("failed-fetching-desired-lrp", err) + return + } + + if request.Update.InstancesExists() { + logger.Debug("updating-lrp-instances") + previousInstanceCount := beforeDesiredLRP.Instances + + requestedInstances := request.Update.GetInstances() - previousInstanceCount + + logger = logger.WithData(lager.Data{"instances_delta": requestedInstances}) + if requestedInstances > 0 { + logger.Debug("increasing-the-instances") + schedulingInfo := desiredLRP.DesiredLRPSchedulingInfo() + h.startInstanceRange(trace.ContextWithRequestId(req), logger, previousInstanceCount, request.Update.GetInstances(), &schedulingInfo) + } + + if requestedInstances < 0 { + logger.Debug("decreasing-the-instances") + numExtraActualLRP := previousInstanceCount + requestedInstances + h.stopInstancesFrom(trace.ContextWithRequestId(req), logger, request.ProcessGuid, int(numExtraActualLRP)) + } + } + + internalRoutesUpdated := request.Update.IsRoutesGroupUpdated(beforeDesiredLRP.Routes, models.InternalRouter) + metricTagsUpdated := request.Update.IsMetricTagsUpdated(beforeDesiredLRP.MetricTags) + + if internalRoutesUpdated || metricTagsUpdated { + h.updateInstances(trace.ContextWithRequestId(req), logger, request.ProcessGuid, request.Update, internalRoutesUpdated, metricTagsUpdated) + } + + go h.desiredHub.Emit(models.NewDesiredLRPChangedEvent(beforeDesiredLRP, desiredLRP, trace.RequestIdFromRequest(req))) +} + +func (h *DesiredLRPHandler) RemoveDesiredLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("remove-desired-lrp").WithTraceInfo(req) + + request := &models.RemoveDesiredLRPRequest{} + response := &models.DesiredLRPLifecycleResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + logger = logger.WithData(lager.Data{"process_guid": request.ProcessGuid}) + + desiredLRP, err := h.desiredLRPDB.DesiredLRPByProcessGuid(req.Context(), logger.Session("fetch-desired"), request.ProcessGuid) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.desiredLRPDB.RemoveDesiredLRP(req.Context(), logger.Session("remove-desired"), request.ProcessGuid) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + go h.desiredHub.Emit(models.NewDesiredLRPRemovedEvent(desiredLRP, trace.RequestIdFromRequest(req))) + + h.stopInstancesFrom(trace.ContextWithRequestId(req), logger, request.ProcessGuid, 0) +} + +func (h *DesiredLRPHandler) startInstanceRange(ctx context.Context, logger lager.Logger, lower, upper int32, schedulingInfo *models.DesiredLRPSchedulingInfo) { + logger = logger.Session("start-instance-range", lager.Data{"lower": lower, "upper": upper}) + logger.Info("starting") + defer logger.Info("complete") + + keys := []*models.ActualLRPKey{} + for actualIndex := lower; actualIndex < upper; actualIndex++ { + key := models.NewActualLRPKey(schedulingInfo.ProcessGuid, int32(actualIndex), schedulingInfo.Domain) + keys = append(keys, &key) + } + + createdIndices := h.createUnclaimedActualLRPs(ctx, logger, keys) + start := auctioneer.NewLRPStartRequestFromSchedulingInfo(schedulingInfo, createdIndices...) + + logger.Info("start-lrp-auction-request", lager.Data{"app_guid": schedulingInfo.ProcessGuid, "indices": createdIndices}) + err := h.auctioneerClient.RequestLRPAuctions(logger, trace.RequestIdFromContext(ctx), []*auctioneer.LRPStartRequest{&start}) + logger.Info("finished-lrp-auction-request", lager.Data{"app_guid": schedulingInfo.ProcessGuid, "indices": createdIndices}) + if err != nil { + logger.Error("failed-to-request-auction", err) + } +} + +func (h *DesiredLRPHandler) createUnclaimedActualLRPs(ctx context.Context, logger lager.Logger, keys []*models.ActualLRPKey) []int { + count := len(keys) + createdIndicesChan := make(chan int, count) + + eventCalculator := calculator.ActualLRPEventCalculator{ + ActualLRPGroupHub: h.actualHub, + ActualLRPInstanceHub: h.actualLRPInstanceHub, + } + + works := make([]func(), count) + logger = logger.Session("create-unclaimed-actual-lrp") + for i, key := range keys { + key := key + works[i] = func() { + logger.Info("starting", lager.Data{"actual_lrp_key": key}) + actualLRP, err := h.actualLRPDB.CreateUnclaimedActualLRP(ctx, logger, key) + if err != nil { + logger.Info("failed", lager.Data{"actual_lrp_key": key, "err_message": err.Error()}) + return + } + + lrps := eventCalculator.RecordChange(nil, actualLRP, nil) + go eventCalculator.EmitEvents(trace.RequestIdFromContext(ctx), nil, lrps) + createdIndicesChan <- int(key.Index) + } + } + + throttlerSize := h.updateWorkersCount + throttler, err := workpool.NewThrottler(throttlerSize, works) + if err != nil { + logger.Error("failed-constructing-throttler", err, lager.Data{"max_workers": throttlerSize, "num_works": len(works)}) + return []int{} + } + + go func() { + throttler.Work() + close(createdIndicesChan) + }() + + createdIndices := make([]int, 0, count) + for createdIndex := range createdIndicesChan { + createdIndices = append(createdIndices, createdIndex) + } + + return createdIndices +} + +func (h *DesiredLRPHandler) stopInstancesFrom(ctx context.Context, logger lager.Logger, processGuid string, index int) { + logger = logger.Session("stop-instances-from", lager.Data{"process_guid": processGuid, "index": index}) + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger.Session("fetch-actuals"), models.ActualLRPFilter{ProcessGuid: processGuid}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err) + return + } + + for i := 0; i < len(actualLRPs); i++ { + lrp := actualLRPs[i] + + if lrp.Presence != models.ActualLRP_Evacuating { + if lrp.Index >= int32(index) { + switch lrp.State { + case models.ActualLRPStateUnclaimed, models.ActualLRPStateCrashed: + err = h.actualLRPDB.RemoveActualLRP(ctx, logger.Session("remove-actual"), lrp.ProcessGuid, lrp.Index, nil) + if err != nil { + logger.Error("failed-removing-lrp-instance", err) + } else { + //lint:ignore SA1019 - implementing deprecated logic until it is removed + go h.actualHub.Emit(models.NewActualLRPRemovedEvent(lrp.ToActualLRPGroup())) + go h.actualLRPInstanceHub.Emit(models.NewActualLRPInstanceRemovedEvent(lrp, trace.RequestIdFromContext(ctx))) + } + default: + cellPresence, err := h.serviceClient.CellById(logger, lrp.CellId) + if err != nil { + logger.Error("failed-fetching-cell-presence", err) + continue + } + repClient, err := h.repClientFactory.CreateClient(cellPresence.RepAddress, cellPresence.RepUrl, trace.RequestIdFromContext(ctx)) + if err != nil { + logger.Error("create-rep-client-failed", err) + continue + } + logger.Debug("stopping-lrp-instance") + go func() { + err := repClient.StopLRPInstance(logger, lrp.ActualLRPKey, lrp.ActualLRPInstanceKey) + if err != nil { + logger.Error("failed-stopping-lrp-instance", err) + } + }() + } + } + } + } +} + +func (h *DesiredLRPHandler) updateInstances(ctx context.Context, logger lager.Logger, processGuid string, update *models.DesiredLRPUpdate, internalRoutesUpdated, metricTagsUpdated bool) { + logger = logger.Session("updating-instances", lager.Data{"process_guid": processGuid}) + actualLRPs, err := h.actualLRPDB.ActualLRPs(ctx, logger.Session("fetch-actuals"), models.ActualLRPFilter{ProcessGuid: processGuid}) + if err != nil { + logger.Error("failed-fetching-actual-lrps", err) + return + } + + for i := 0; i < len(actualLRPs); i++ { + lrp := actualLRPs[i] + + if lrp.Presence != models.ActualLRP_Evacuating && lrp.State != models.ActualLRPStateUnclaimed && lrp.State != models.ActualLRPStateCrashed { + cellPresence, err := h.serviceClient.CellById(logger, lrp.CellId) + if err != nil { + logger.Error("failed-fetching-cell-presence", err) + continue + } + repClient, err := h.repClientFactory.CreateClient(cellPresence.RepAddress, cellPresence.RepUrl, trace.RequestIdFromContext(ctx)) + if err != nil { + logger.Error("create-rep-client-failed", err) + continue + } + logger.Debug("updating-lrp-instance") + + var internalRoutes models.InternalRoutes + if internalRoutesUpdated { + internalRoutes, err = models.InternalRoutesFromRoutingInfo(*update.Routes) + if err != nil { + logger.Error("getting-internal-routes-failed", err) + continue + } + } + + var metricTags map[string]string + if metricTagsUpdated { + metricTags, err = models.ConvertMetricTags(update.MetricTags, map[models.MetricTagValue_DynamicValue]interface{}{ + models.MetricTagDynamicValueIndex: lrp.Index, + models.MetricTagDynamicValueInstanceGuid: lrp.InstanceGuid, + }) + if err != nil { + logger.Error("converting-metric-tags-failed", err) + continue + } + } + + lrpUpdate := rep.NewLRPUpdate(lrp.ActualLRPInstanceKey.InstanceGuid, lrp.ActualLRPKey, internalRoutes, metricTags) + go func() { + err := repClient.UpdateLRPInstance(logger, lrpUpdate) + if err != nil { + logger.Error("updating-lrp-instance", err) + } + }() + } + } +} + +func (h *DesiredLRPHandler) logDesiredLrpParsingErrors(err *models.Error, processGuid string) error { + appGuid := parseAppGuidFromProcessGuid(processGuid) + if appGuid == "" { + return errors.New("app guid is empty") + } + + tags := map[string]string{ + "source_id": appGuid, + } + + return h.metronClient.SendAppErrorLog( + fmt.Sprintf("Error parsing request for app with guid %s, %s, %s", appGuid, err.GetType(), err.GetMessage()), + BbsLogSource, + tags) +} + +// Parses a ProcessGuid which is in the format: 'UUID-UUID' and returns the first part which is the AppGuid +func parseAppGuidFromProcessGuid(processGuid string) string { + const uuidParts = 5 // a valid UUID contains 5 parts separated by '-' + + parts := strings.Split(processGuid, "-") + if len(parts) < uuidParts { + return "" + } + + return strings.Join(parts[:uuidParts], "-") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/domain_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/domain_handlers.go new file mode 100644 index 0000000000..6951e24dfb --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/domain_handlers.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "errors" + "net/http" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +type DomainHandler struct { + db db.DomainDB + exitChan chan<- struct{} +} + +var ( + ErrDomainMissing = errors.New("domain missing from request") + ErrMaxAgeMissing = errors.New("max-age directive missing from request") +) + +func NewDomainHandler(db db.DomainDB, exitChan chan<- struct{}) *DomainHandler { + return &DomainHandler{ + db: db, + exitChan: exitChan, + } +} + +func (h *DomainHandler) Domains(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("domains").WithTraceInfo(req) + response := &models.DomainsResponse{} + response.Domains, err = h.db.FreshDomains(req.Context(), logger) + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} + +func (h *DomainHandler) Upsert(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("upsert").WithTraceInfo(req) + + request := &models.UpsertDomainRequest{} + response := &models.UpsertDomainResponse{} + + err = parseRequest(logger, req, request) + if err == nil { + err = h.db.UpsertDomain(req.Context(), logger, request.Domain, request.Ttl) + } + + response.Error = models.ConvertError(err) + writeResponse(w, response) + exitIfUnrecoverable(logger, h.exitChan, response.Error) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/evacuation_handler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/evacuation_handler.go new file mode 100644 index 0000000000..03c9a928d5 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/evacuation_handler.go @@ -0,0 +1,171 @@ +package handlers + +import ( + "context" + "net/http" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" + "github.com/gogo/protobuf/proto" +) + +//counterfeiter:generate -o fake_controllers/fake_evacuation_controller.go . EvacuationController +type EvacuationController interface { + RemoveEvacuatingActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey) error + EvacuateClaimedActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey) (bool, error) + EvacuateCrashedActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey, string) error + EvacuateRunningActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey, *models.ActualLRPNetInfo, []*models.ActualLRPInternalRoute, map[string]string, bool, string) (bool, error) + EvacuateStoppedActualLRP(context.Context, lager.Logger, *models.ActualLRPKey, *models.ActualLRPInstanceKey) error +} + +type EvacuationHandler struct { + controller EvacuationController + exitChan chan<- struct{} +} + +func NewEvacuationHandler( + controller EvacuationController, + exitChan chan<- struct{}, +) *EvacuationHandler { + return &EvacuationHandler{ + controller: controller, + exitChan: exitChan, + } +} + +type MessageValidator interface { + proto.Message + Validate() error + Unmarshal(data []byte) error +} + +func (h *EvacuationHandler) RemoveEvacuatingActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("remove-evacuating-actual-lrp").WithTraceInfo(req) + logger.Info("started") + defer logger.Info("completed") + + request := &models.RemoveEvacuatingActualLRPRequest{} + response := &models.RemoveEvacuatingActualLRPResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + err = h.controller.RemoveEvacuatingActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey) + response.Error = models.ConvertError(err) +} + +func (h *EvacuationHandler) EvacuateClaimedActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("evacuate-claimed-actual-lrp").WithTraceInfo(req) + logger.Info("started") + defer logger.Info("completed") + + request := &models.EvacuateClaimedActualLRPRequest{} + response := &models.EvacuationResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + response.KeepContainer = true + return + } + + keepContainer, err := h.controller.EvacuateClaimedActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey) + response.Error = models.ConvertError(err) + response.KeepContainer = keepContainer +} + +func (h *EvacuationHandler) EvacuateCrashedActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("evacuate-crashed-actual-lrp").WithTraceInfo(req) + logger.Info("started") + defer logger.Info("completed") + + request := &models.EvacuateCrashedActualLRPRequest{} + response := &models.EvacuationResponse{} + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.EvacuateCrashedActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey, request.ErrorMessage) + response.Error = models.ConvertError(err) +} + +func (h *EvacuationHandler) commonEvacuateRunningActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request, useInternalRoutesAndTags bool) { + logger = logger.Session("evacuate-running-actual-lrp").WithTraceInfo(req) + logger.Info("starting") + defer logger.Info("completed") + + response := &models.EvacuationResponse{} + response.KeepContainer = true + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + request := &models.EvacuateRunningActualLRPRequest{} + err := parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + return + } + + var keepContainer bool + var actualLrpInternalRoutes []*models.ActualLRPInternalRoute + var metricTags map[string]string + if useInternalRoutesAndTags { + actualLrpInternalRoutes = request.ActualLrpInternalRoutes + metricTags = request.MetricTags + } + routable := true + if request.RoutableExists() { + r := request.GetRoutable() + routable = r + } + + keepContainer, err = h.controller.EvacuateRunningActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey, request.ActualLrpNetInfo, actualLrpInternalRoutes, metricTags, routable, request.AvailabilityZone) + response.Error = models.ConvertError(err) + response.KeepContainer = keepContainer +} + +func (h *EvacuationHandler) EvacuateRunningActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + useInternalRoutesAndTags := true + h.commonEvacuateRunningActualLRP(logger, w, req, useInternalRoutesAndTags) +} + +func (h *EvacuationHandler) EvacuateRunningActualLRP_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + useInternalRoutesAndTags := false + h.commonEvacuateRunningActualLRP(logger, w, req, useInternalRoutesAndTags) +} + +func (h *EvacuationHandler) EvacuateStoppedActualLRP(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("evacuate-stopped-actual-lrp").WithTraceInfo(req) + + request := &models.EvacuateStoppedActualLRPRequest{} + response := &models.EvacuationResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer writeResponse(w, response) + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-to-parse-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.EvacuateStoppedActualLRP(req.Context(), logger, request.ActualLrpKey, request.ActualLrpInstanceKey) + response.Error = models.ConvertError(err) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers.go new file mode 100644 index 0000000000..929eba8147 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers.go @@ -0,0 +1,138 @@ +package handlers + +import ( + "bytes" + "fmt" + "net/http" + + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +type EventController interface { + Subscribe_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) + Subscribe_r1(logger lager.Logger, w http.ResponseWriter, req *http.Request) +} + +// Deprecated: use LRPInstanceEventHandler instead +type LRPGroupEventsHandler struct { + desiredHub events.Hub + actualHub events.Hub +} + +type TaskEventHandler struct { + taskHub events.Hub +} + +type LRPInstanceEventHandler struct { + desiredHub events.Hub + lrpInstanceHub events.Hub +} + +// Deprecated: use LRPInstanceEventHandler instead +func NewLRPGroupEventsHandler(desiredHub, actualHub events.Hub) *LRPGroupEventsHandler { + return &LRPGroupEventsHandler{ + desiredHub: desiredHub, + actualHub: actualHub, + } +} + +func NewTaskEventHandler(taskHub events.Hub) *TaskEventHandler { + return &TaskEventHandler{ + taskHub: taskHub, + } +} + +func NewLRPInstanceEventHandler(desiredHub, lrpInstanceHub events.Hub) *LRPInstanceEventHandler { + return &LRPInstanceEventHandler{ + desiredHub: desiredHub, + lrpInstanceHub: lrpInstanceHub, + } +} + +func streamEventsToResponse(logger lager.Logger, w http.ResponseWriter, eventChan <-chan models.Event, errorChan <-chan error) { + w.Header().Add("Content-Type", "text/event-stream; charset=utf-8") + w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Add("Connection", "keep-alive") + + w.WriteHeader(http.StatusOK) + + conn, rw, err := w.(http.Hijacker).Hijack() + if err != nil { + return + } + + defer func() { + fmt.Fprintf(conn, "0\r\n\r\n") + err := conn.Close() + if err != nil { + logger.Error("failed-to-close-connection", err) + } + }() + + if err := rw.Flush(); err != nil { + logger.Error("failed-to-flush", err) + return + } + + var event models.Event + eventID := 0 + done := make(chan bool, 1) + go func() { + // #nosec G104 - ignore errors when reading hijacked HTTP requests so we don't spam our logs during a DoS + rw.ReadFrom(conn) + done <- true + }() + + for { + select { + case event = <-eventChan: + case err := <-errorChan: + logger.Error("failed-to-get-next-event", err) + return + case <-done: + logger.Debug("received-close-notify") + return + } + + sseEvent, err := events.NewEventFromModelEvent(eventID, event) + if err != nil { + logger.Error("failed-to-marshal-event", err) + return + } + + buf := new(bytes.Buffer) + + err = sseEvent.Write(buf) + if err != nil { + logger.Error("failed-to-write-event", err) + return + } + + fmt.Fprintf(conn, "%x;\r\n", buf.Len()) + fmt.Fprintf(conn, "%s\r\n", buf.String()) + + eventID++ + } +} + +type EventFetcher func() (models.Event, error) + +func streamSource(eventChan chan<- models.Event, errorChan chan<- error, closeChan chan struct{}, fetchEvent EventFetcher) { + for { + event, err := fetchEvent() + if err != nil { + select { + case errorChan <- err: + case <-closeChan: + } + return + } + select { + case eventChan <- event: + case <-closeChan: + return + } + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers_r0.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers_r0.go new file mode 100644 index 0000000000..98fca66f58 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/events_handlers_r0.go @@ -0,0 +1,274 @@ +package handlers + +import ( + "net/http" + + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +func (h *LRPGroupEventsHandler) commonSubscribe(logger lager.Logger, w http.ResponseWriter, req *http.Request, target format.Version) { + logger = logger.Session("subscribe-r0").WithTraceInfo(req) + + request := &models.EventsByCellId{} + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + logger.Info("subscribed-to-event-stream", lager.Data{"cell_id": request.CellId}) + + desiredSource, err := h.desiredHub.Subscribe() + if err != nil { + logger.Error("failed-to-subscribe-to-desired-event-hub", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer desiredSource.Close() + + actualSource, err := h.actualHub.Subscribe() + if err != nil { + logger.Error("failed-to-subscribe-to-actual-event-hub", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer actualSource.Close() + + eventChan := make(chan models.Event) + errorChan := make(chan error) + closeChan := make(chan struct{}) + defer close(closeChan) + + actualEventsFetcher := actualSource.Next + if request.CellId != "" { + actualEventsFetcher = func() (models.Event, error) { + for { + event, err := actualSource.Next() + if err != nil { + return event, err + } + + if matches, err := filterByCellID(request.CellId, event, err); err != nil { + return nil, err + } else if matches { + return event, nil + } + } + } + } + + desiredEventsFetcher := func() (models.Event, error) { + event, err := desiredSource.Next() + if err != nil { + return event, err + } + event = models.VersionDesiredLRPsTo(event, target) + return event, err + } + + go streamSource(eventChan, errorChan, closeChan, desiredEventsFetcher) + go streamSource(eventChan, errorChan, closeChan, actualEventsFetcher) + + streamEventsToResponse(logger, w, eventChan, errorChan) +} + +func (h *LRPGroupEventsHandler) Subscribe_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V0) +} + +func (h *LRPGroupEventsHandler) Subscribe_r1(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V3) +} + +func (h *LRPInstanceEventHandler) commonSubscribe(logger lager.Logger, w http.ResponseWriter, req *http.Request, target format.Version) { + logger = logger.Session("subscribe-r0").WithTraceInfo(req) + + request := &models.EventsByCellId{} + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + logger.Info("subscribed-to-instance-event-stream", lager.Data{"cell_id": request.CellId}) + + desiredSource, err := h.desiredHub.Subscribe() + if err != nil { + logger.Error("failed-to-subscribe-to-desired-event-hub", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer desiredSource.Close() + + lrpInstanceSource, err := h.lrpInstanceHub.Subscribe() + if err != nil { + logger.Error("failed-to-subscribe-to-actual-instance-event-hub", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer lrpInstanceSource.Close() + + eventChan := make(chan models.Event) + errorChan := make(chan error) + closeChan := make(chan struct{}) + defer close(closeChan) + + lrpInstanceEventFetcher := lrpInstanceSource.Next + if request.CellId != "" { + lrpInstanceEventFetcher = func() (models.Event, error) { + for { + event, err := lrpInstanceSource.Next() + if err != nil { + return event, err + } + + if filterInstanceEventByCellID(request.CellId, event, err) { + return event, nil + } + } + } + } + + desiredEventsFetcher := func() (models.Event, error) { + event, err := desiredSource.Next() + if err != nil { + return event, err + } + event = models.VersionDesiredLRPsTo(event, target) + return event, err + } + + go streamSource(eventChan, errorChan, closeChan, desiredEventsFetcher) + go streamSource(eventChan, errorChan, closeChan, lrpInstanceEventFetcher) + + streamEventsToResponse(logger, w, eventChan, errorChan) +} + +func (h *LRPInstanceEventHandler) Subscribe_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V0) +} + +func (h *LRPInstanceEventHandler) Subscribe_r1(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V3) +} + +func (h *TaskEventHandler) commonSubscribe(logger lager.Logger, w http.ResponseWriter, req *http.Request, target format.Version) { + logger = logger.Session("tasks-subscribe-r0").WithTraceInfo(req) + logger.Info("subscribed-to-tasks-event-stream") + + taskSource, err := h.taskHub.Subscribe() + if err != nil { + logger.Error("failed-to-subscribe-to-task-event-hub", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer taskSource.Close() + + eventChan := make(chan models.Event) + errorChan := make(chan error) + closeChan := make(chan struct{}) + defer close(closeChan) + + taskEventsFetcher := func() (models.Event, error) { + event, err := taskSource.Next() + if err != nil { + return event, err + } + event = models.VersionTaskDefinitionsTo(event, target) + return event, err + } + + go streamSource(eventChan, errorChan, closeChan, taskEventsFetcher) + + streamEventsToResponse(logger, w, eventChan, errorChan) +} + +func (h *TaskEventHandler) Subscribe_r0(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V0) +} + +func (h *TaskEventHandler) Subscribe_r1(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonSubscribe(logger, w, req, format.V3) +} + +func filterByCellID(cellID string, bbsEvent models.Event, err error) (bool, error) { + switch x := bbsEvent.(type) { + //lint:ignore SA1019 - need to support this event until the deprecation becomes deletion + case *models.ActualLRPCreatedEvent: + //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method + lrp, _, resolveError := x.ActualLrpGroup.Resolve() + if resolveError != nil { + return false, resolveError + } + + if lrp.CellId != cellID { + return false, nil + } + + //lint:ignore SA1019 - need to support this event until the deprecation becomes deletion + case *models.ActualLRPChangedEvent: + //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method + beforeLRP, _, beforeResolveError := x.Before.Resolve() + if beforeResolveError != nil { + return false, beforeResolveError + } + //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method + afterLRP, _, afterResolveError := x.After.Resolve() + if afterResolveError != nil { + return false, afterResolveError + } + if afterLRP.CellId != cellID && beforeLRP.CellId != cellID { + return false, nil + } + + //lint:ignore SA1019 - need to support this event until the deprecation becomes deletion + case *models.ActualLRPRemovedEvent: + //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method + lrp, _, resolveError := x.ActualLrpGroup.Resolve() + if resolveError != nil { + return false, resolveError + } + if lrp.CellId != cellID { + return false, nil + } + + case *models.ActualLRPCrashedEvent: + if x.ActualLRPInstanceKey.CellId != cellID { + return false, nil + } + } + + return true, nil +} + +func filterInstanceEventByCellID(cellID string, bbsEvent models.Event, err error) bool { + switch x := bbsEvent.(type) { + case *models.ActualLRPInstanceCreatedEvent: + lrp := x.ActualLrp + if lrp.CellId != cellID { + return false + } + + case *models.ActualLRPInstanceChangedEvent: + if x.CellId != cellID { + return false + } + + case *models.ActualLRPInstanceRemovedEvent: + lrp := x.ActualLrp + if lrp.CellId != cellID { + return false + } + + case *models.ActualLRPCrashedEvent: + if x.ActualLRPInstanceKey.CellId != cellID { + return false + } + } + + return true +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/handlers.go new file mode 100644 index 0000000000..827d5baa86 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/handlers.go @@ -0,0 +1,212 @@ +package handlers + +import ( + "io" + "net/http" + "strconv" + + "code.cloudfoundry.org/auctioneer" + "code.cloudfoundry.org/bbs" + "code.cloudfoundry.org/bbs/cmd/bbs/config" + "code.cloudfoundry.org/bbs/controllers" + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/handlers/middleware" + "code.cloudfoundry.org/bbs/metrics" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/serviceclient" + "code.cloudfoundry.org/bbs/taskworkpool" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/rep" + "github.com/gogo/protobuf/proto" + "github.com/tedsuo/rata" +) + +func New( + logger, + accessLogger lager.Logger, + updateWorkers int, + convergenceWorkersSize int, + maxTaskPlacementRetries int, + advancedMetricsConfig config.AdvancedMetrics, + emitter middleware.Emitter, + db db.DB, + desiredHub, actualHub, actualLRPInstanceHub, taskHub events.Hub, + taskCompletionClient taskworkpool.TaskCompletionClient, + serviceClient serviceclient.ServiceClient, + auctioneerClient auctioneer.Client, + repClientFactory rep.ClientFactory, + taskStatMetronNotifier metrics.TaskStatMetronNotifier, + migrationsDone <-chan struct{}, + exitChan chan struct{}, + metronClient loggingclient.IngressClient, +) http.Handler { + pingHandler := NewPingHandler() + domainHandler := NewDomainHandler(db, exitChan) + actualLRPHandler := NewActualLRPHandler(db, exitChan) + actualLRPController := controllers.NewActualLRPLifecycleController( + db, db, db, db, + auctioneerClient, + serviceClient, + repClientFactory, + actualHub, + actualLRPInstanceHub, + ) + evacuationController := controllers.NewEvacuationController( + db, db, db, db, + auctioneerClient, + actualHub, + actualLRPInstanceHub, + ) + actualLRPLifecycleHandler := NewActualLRPLifecycleHandler(actualLRPController, exitChan) + evacuationHandler := NewEvacuationHandler(evacuationController, exitChan) + desiredLRPHandler := NewDesiredLRPHandler(updateWorkers, db, db, desiredHub, actualHub, actualLRPInstanceHub, auctioneerClient, repClientFactory, serviceClient, exitChan, metronClient) + taskController := controllers.NewTaskController(db, taskCompletionClient, auctioneerClient, serviceClient, repClientFactory, taskHub, taskStatMetronNotifier, maxTaskPlacementRetries) + taskHandler := NewTaskHandler(taskController, exitChan) + lrpGroupEventsHandler := NewLRPGroupEventsHandler(desiredHub, actualHub) + taskEventsHandler := NewTaskEventHandler(taskHub) + lrpInstanceEventsHandler := NewLRPInstanceEventHandler(desiredHub, actualLRPInstanceHub) + cellsHandler := NewCellHandler(serviceClient, exitChan) + + metricsAndLoggingWrap := func(loggableHandlerFunc middleware.LoggableHandlerFunc, routeName string) http.HandlerFunc { + return middleware.RecordMetrics(middleware.LogWrap(logger, accessLogger, loggableHandlerFunc), emitter, advancedMetricsConfig, routeName) + } + + actions := rata.Handlers{ + // Ping + bbs.PingRoute_r0: metricsAndLoggingWrap(pingHandler.Ping, bbs.PingRoute_r0), + + // Domains + bbs.DomainsRoute_r0: metricsAndLoggingWrap(domainHandler.Domains, bbs.DomainsRoute_r0), + bbs.UpsertDomainRoute_r0: metricsAndLoggingWrap(domainHandler.Upsert, bbs.UpsertDomainRoute_r0), + + // Actual LRPs + bbs.ActualLRPsRoute_r0: metricsAndLoggingWrap(actualLRPHandler.ActualLRPs, bbs.ActualLRPsRoute_r0), + // Multiple Actual LRPs by multiple Process GUIDs + bbs.ActualLRPsByProcessGuidsRoute_r0: metricsAndLoggingWrap(actualLRPHandler.ActualLRPsByProcessGuids, bbs.ActualLRPsByProcessGuidsRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.ActualLRPGroupsRoute_r0: metricsAndLoggingWrap(actualLRPHandler.ActualLRPGroups, bbs.ActualLRPGroupsRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.ActualLRPGroupsByProcessGuidRoute_r0: metricsAndLoggingWrap(actualLRPHandler.ActualLRPGroupsByProcessGuid, bbs.ActualLRPGroupsByProcessGuidRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.ActualLRPGroupByProcessGuidAndIndexRoute_r0: metricsAndLoggingWrap(actualLRPHandler.ActualLRPGroupByProcessGuidAndIndex, bbs.ActualLRPGroupByProcessGuidAndIndexRoute_r0), + + // Actual LRP Lifecycle + bbs.ClaimActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.ClaimActualLRP, bbs.ClaimActualLRPRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.StartActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.StartActualLRP_r0, bbs.StartActualLRPRoute_r0), // DEPRECATED + bbs.StartActualLRPRoute_r1: metricsAndLoggingWrap(actualLRPLifecycleHandler.StartActualLRP, bbs.StartActualLRPRoute_r1), + bbs.CrashActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.CrashActualLRP, bbs.CrashActualLRPRoute_r0), + bbs.RetireActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.RetireActualLRP, bbs.RetireActualLRPRoute_r0), + bbs.FailActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.FailActualLRP, bbs.FailActualLRPRoute_r0), + bbs.RemoveActualLRPRoute_r0: metricsAndLoggingWrap(actualLRPLifecycleHandler.RemoveActualLRP, bbs.RemoveActualLRPRoute_r0), + + // Evacuation + bbs.RemoveEvacuatingActualLRPRoute_r0: metricsAndLoggingWrap(evacuationHandler.RemoveEvacuatingActualLRP, bbs.RemoveEvacuatingActualLRPRoute_r0), + bbs.EvacuateClaimedActualLRPRoute_r0: metricsAndLoggingWrap(evacuationHandler.EvacuateClaimedActualLRP, bbs.EvacuateClaimedActualLRPRoute_r0), + bbs.EvacuateCrashedActualLRPRoute_r0: metricsAndLoggingWrap(evacuationHandler.EvacuateCrashedActualLRP, bbs.EvacuateCrashedActualLRPRoute_r0), + bbs.EvacuateStoppedActualLRPRoute_r0: metricsAndLoggingWrap(evacuationHandler.EvacuateStoppedActualLRP, bbs.EvacuateStoppedActualLRPRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.EvacuateRunningActualLRPRoute_r0: metricsAndLoggingWrap(evacuationHandler.EvacuateRunningActualLRP_r0, bbs.EvacuateRunningActualLRPRoute_r0), // DEPRECATED + bbs.EvacuateRunningActualLRPRoute_r1: metricsAndLoggingWrap(evacuationHandler.EvacuateRunningActualLRP, bbs.EvacuateRunningActualLRPRoute_r1), + + // Desired LRPs + bbs.DesiredLRPsRoute_r3: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPs, bbs.DesiredLRPsRoute_r3), + bbs.DesiredLRPByProcessGuidRoute_r3: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPByProcessGuid, bbs.DesiredLRPByProcessGuidRoute_r3), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.DesiredLRPsRoute_r2: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPs_r2, bbs.DesiredLRPsRoute_r2), // DEPRECATED + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.DesiredLRPByProcessGuidRoute_r2: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPByProcessGuid_r2, bbs.DesiredLRPByProcessGuidRoute_r2), // DEPRECATED + bbs.DesiredLRPSchedulingInfosRoute_r0: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPSchedulingInfos, bbs.DesiredLRPSchedulingInfosRoute_r0), + bbs.DesiredLRPSchedulingInfoByProcessGuid_r0: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPSchedulingInfoByProcessGuid, bbs.DesiredLRPSchedulingInfoByProcessGuid_r0), + bbs.DesiredLRPRoutingInfosRoute_r0: metricsAndLoggingWrap(desiredLRPHandler.DesiredLRPRoutingInfos, bbs.DesiredLRPRoutingInfosRoute_r0), + bbs.DesireDesiredLRPRoute_r2: metricsAndLoggingWrap(desiredLRPHandler.DesireDesiredLRP, bbs.DesireDesiredLRPRoute_r2), + bbs.UpdateDesiredLRPRoute_r0: metricsAndLoggingWrap(desiredLRPHandler.UpdateDesiredLRP, bbs.UpdateDesiredLRPRoute_r0), + bbs.RemoveDesiredLRPRoute_r0: metricsAndLoggingWrap(desiredLRPHandler.RemoveDesiredLRP, bbs.RemoveDesiredLRPRoute_r0), + + // Tasks + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.TasksRoute_r2: metricsAndLoggingWrap(taskHandler.Tasks_r2, bbs.TasksRoute_r2), // DEPRECATED + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.TaskByGuidRoute_r2: metricsAndLoggingWrap(taskHandler.TaskByGuid_r2, bbs.TaskByGuidRoute_r2), // DEPRECATED + bbs.TasksRoute_r3: metricsAndLoggingWrap(taskHandler.Tasks, bbs.TasksRoute_r3), + bbs.TaskByGuidRoute_r3: metricsAndLoggingWrap(taskHandler.TaskByGuid, bbs.TaskByGuidRoute_r3), + bbs.DesireTaskRoute_r2: metricsAndLoggingWrap(taskHandler.DesireTask, bbs.DesireTaskRoute_r2), + bbs.StartTaskRoute_r0: metricsAndLoggingWrap(taskHandler.StartTask, bbs.StartTaskRoute_r0), + bbs.CancelTaskRoute_r0: metricsAndLoggingWrap(taskHandler.CancelTask, bbs.CancelTaskRoute_r0), + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.FailTaskRoute_r0: metricsAndLoggingWrap(taskHandler.FailTask, bbs.FailTaskRoute_r0), // DEPRECATED + bbs.RejectTaskRoute_r0: metricsAndLoggingWrap(taskHandler.RejectTask, bbs.RejectTaskRoute_r0), + bbs.CompleteTaskRoute_r0: metricsAndLoggingWrap(taskHandler.CompleteTask, bbs.CompleteTaskRoute_r0), + bbs.ResolvingTaskRoute_r0: metricsAndLoggingWrap(taskHandler.ResolvingTask, bbs.ResolvingTaskRoute_r0), + bbs.DeleteTaskRoute_r0: metricsAndLoggingWrap(taskHandler.DeleteTask, bbs.DeleteTaskRoute_r0), + + // Events + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.EventStreamRoute_r0: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, lrpGroupEventsHandler.Subscribe_r0), emitter), // DEPRECATED + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.TaskEventStreamRoute_r0: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, taskEventsHandler.Subscribe_r0), emitter), // DEPRECATED + //lint:ignore SA1019 - implementing deprecated logic until it is removed + bbs.LrpInstanceEventStreamRoute_r0: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, lrpInstanceEventsHandler.Subscribe_r0), emitter), // DEPRECATED + bbs.LRPGroupEventStreamRoute_r1: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, lrpGroupEventsHandler.Subscribe_r1), emitter), + bbs.TaskEventStreamRoute_r1: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, taskEventsHandler.Subscribe_r1), emitter), + bbs.LRPInstanceEventStreamRoute_r1: middleware.RecordRequestCount(middleware.LogWrap(logger, accessLogger, lrpInstanceEventsHandler.Subscribe_r1), emitter), + + // Cells + bbs.CellsRoute_r0: metricsAndLoggingWrap(cellsHandler.Cells, bbs.CellsRoute_r0), + } + + handler, err := rata.NewRouter(bbs.Routes, actions) + if err != nil { + panic("unable to create router: " + err.Error()) + } + + return UnavailableWrap(handler, + migrationsDone, + ) +} + +func parseRequest(logger lager.Logger, req *http.Request, request MessageValidator) error { + data, err := io.ReadAll(req.Body) + if err != nil { + logger.Error("failed-to-read-body", err) + return models.ErrUnknownError + } + + err = request.Unmarshal(data) + if err != nil { + logger.Error("failed-to-parse-request-body", err) + return models.ErrBadRequest + } + + if err := request.Validate(); err != nil { + logger.Error("invalid-request", err) + return models.NewError(models.Error_InvalidRequest, err.Error()) + } + + return nil +} + +func exitIfUnrecoverable(logger lager.Logger, exitCh chan<- struct{}, err *models.Error) { + if err != nil && err.Type == models.Error_Unrecoverable { + logger.Error("unrecoverable-error", err) + select { + case exitCh <- struct{}{}: + default: + } + } +} + +func writeResponse(w http.ResponseWriter, message proto.Message) { + responseBytes, err := proto.Marshal(message) + if err != nil { + panic("Unable to encode Proto: " + err.Error()) + } + + w.Header().Set("Content-Length", strconv.Itoa(len(responseBytes))) + w.Header().Set("Content-Type", "application/x-protobuf") + w.WriteHeader(http.StatusOK) + // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS + w.Write(responseBytes) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/package.go new file mode 100644 index 0000000000..b735547277 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/package.go @@ -0,0 +1 @@ +package handlers // import "code.cloudfoundry.org/bbs/handlers" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/ping_handler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/ping_handler.go new file mode 100644 index 0000000000..e76f81f493 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/ping_handler.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "net/http" + + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/lager/v3" +) + +type PingHandler struct { +} + +func NewPingHandler() *PingHandler { + return &PingHandler{} +} + +func (h *PingHandler) Ping(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + response := &models.PingResponse{} + response.Available = true + writeResponse(w, response) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/task_handlers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/task_handlers.go new file mode 100644 index 0000000000..ccff42f487 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/task_handlers.go @@ -0,0 +1,281 @@ +package handlers + +import ( + "context" + "net/http" + "time" + + "code.cloudfoundry.org/bbs/format" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/bbs/trace" + "code.cloudfoundry.org/lager/v3" +) + +//counterfeiter:generate -o fake_controllers/fake_task_controller.go . TaskController + +type TaskController interface { + Tasks(ctx context.Context, logger lager.Logger, domain, cellId string) ([]*models.Task, error) + TaskByGuid(ctx context.Context, logger lager.Logger, taskGuid string) (*models.Task, error) + DesireTask(ctx context.Context, logger lager.Logger, taskDefinition *models.TaskDefinition, taskGuid, domain string) error + StartTask(ctx context.Context, logger lager.Logger, taskGuid, cellId string) (shouldStart bool, err error) + CancelTask(ctx context.Context, logger lager.Logger, taskGuid string) error + FailTask(ctx context.Context, logger lager.Logger, taskGuid, failureReason string) error + RejectTask(ctx context.Context, logger lager.Logger, taskGuid, failureReason string) error + CompleteTask(ctx context.Context, logger lager.Logger, taskGuid, cellId string, failed bool, failureReason, result string) error + ResolvingTask(ctx context.Context, logger lager.Logger, taskGuid string) error + DeleteTask(ctx context.Context, logger lager.Logger, taskGuid string) error + ConvergeTasks(ctx context.Context, logger lager.Logger, kickTaskDuration, expirePendingTaskDuration, expireCompletedTaskDuration time.Duration) error +} + +type TaskHandler struct { + controller TaskController + exitChan chan<- struct{} +} + +func NewTaskHandler( + controller TaskController, + exitChan chan<- struct{}, +) *TaskHandler { + return &TaskHandler{ + controller: controller, + exitChan: exitChan, + } +} + +func (h *TaskHandler) commonTasks(logger lager.Logger, targetVersion format.Version, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("tasks").WithTraceInfo(req) + + request := &models.TasksRequest{} + response := &models.TasksResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + tasks, err := h.controller.Tasks(req.Context(), logger, request.Domain, request.CellId) + + downgradedTasks := []*models.Task{} + for _, t := range tasks { + downgradedTasks = append(downgradedTasks, t.VersionDownTo(targetVersion)) + } + response.Tasks = downgradedTasks + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) Tasks_r2(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonTasks(logger, format.V2, w, req) +} + +func (h *TaskHandler) Tasks(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonTasks(logger, format.V3, w, req) +} + +func (h *TaskHandler) commonTaskByGuid(logger lager.Logger, targetVersion format.Version, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("task-by-guid").WithTraceInfo(req) + + request := &models.TaskByGuidRequest{} + response := &models.TaskResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + var task *models.Task + task, err = h.controller.TaskByGuid(req.Context(), logger, request.TaskGuid) + if task != nil { + task = task.VersionDownTo(targetVersion) + } + + response.Task = task + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) TaskByGuid_r2(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonTaskByGuid(logger, format.V2, w, req) +} + +func (h *TaskHandler) TaskByGuid(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + h.commonTaskByGuid(logger, format.V3, w, req) +} + +func (h *TaskHandler) DesireTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("desire-task").WithTraceInfo(req) + + request := &models.DesireTaskRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.DesireTask(req.Context(), logger, request.TaskDefinition, request.TaskGuid, request.Domain) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) StartTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("start-task").WithTraceInfo(req) + + request := &models.StartTaskRequest{} + response := &models.StartTaskResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + response.ShouldStart, err = h.controller.StartTask(req.Context(), logger, request.TaskGuid, request.CellId) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) CancelTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + logger = logger.Session("cancel-task").WithTraceInfo(req) + + request := &models.TaskGuidRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err := parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.CancelTask(trace.ContextWithRequestId(req), logger, request.TaskGuid) + response.Error = models.ConvertError(err) +} + +// Deprecated: do not use +func (h *TaskHandler) FailTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("fail-task").WithTraceInfo(req) + + request := &models.FailTaskRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.FailTask(req.Context(), logger, request.TaskGuid, request.FailureReason) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) RejectTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("reject-task").WithTraceInfo(req) + + request := &models.RejectTaskRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.RejectTask(req.Context(), logger, request.TaskGuid, request.RejectionReason) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) CompleteTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("complete-task").WithTraceInfo(req) + + request := &models.CompleteTaskRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + response.Error = models.ConvertError(err) + logger.Error("failed-parsing-request", err) + return + } + + err = h.controller.CompleteTask(req.Context(), logger, request.TaskGuid, request.CellId, request.Failed, request.FailureReason, request.Result) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) ResolvingTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("resolving-task").WithTraceInfo(req) + + request := &models.TaskGuidRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.ResolvingTask(req.Context(), logger, request.TaskGuid) + response.Error = models.ConvertError(err) +} + +func (h *TaskHandler) DeleteTask(logger lager.Logger, w http.ResponseWriter, req *http.Request) { + var err error + logger = logger.Session("delete-task").WithTraceInfo(req) + + request := &models.TaskGuidRequest{} + response := &models.TaskLifecycleResponse{} + + defer func() { exitIfUnrecoverable(logger, h.exitChan, response.Error) }() + defer func() { writeResponse(w, response) }() + + err = parseRequest(logger, req, request) + if err != nil { + logger.Error("failed-parsing-request", err) + response.Error = models.ConvertError(err) + return + } + + err = h.controller.DeleteTask(req.Context(), logger, request.TaskGuid) + response.Error = models.ConvertError(err) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/unavailable_handler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/unavailable_handler.go new file mode 100644 index 0000000000..2b7337035b --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/handlers/unavailable_handler.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "net/http" + "sync" +) + +type UnavailableHandler struct { + handler http.Handler + waitCh <-chan struct{} +} + +func NewUnavailableHandler(handler http.Handler, serviceReadyChan ...<-chan struct{}) *UnavailableHandler { + wg := sync.WaitGroup{} + for _, ch := range serviceReadyChan { + wg.Add(1) + go func(ch <-chan struct{}) { + defer wg.Done() + <-ch + }(ch) + } + + waitCh := make(chan struct{}) + go func() { + wg.Wait() + close(waitCh) + }() + + u := &UnavailableHandler{ + handler: handler, + waitCh: waitCh, + } + + return u +} + +func (u *UnavailableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + select { + case <-u.waitCh: + u.handler.ServeHTTP(w, r) + default: + w.WriteHeader(http.StatusServiceUnavailable) + } +} + +func UnavailableWrap(handler http.Handler, serviceReady ...<-chan struct{}) http.HandlerFunc { + handler = NewUnavailableHandler(handler, serviceReady...) + + return func(w http.ResponseWriter, r *http.Request) { + handler.ServeHTTP(w, r) + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/bbs_election_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/bbs_election_metron_notifier.go new file mode 100644 index 0000000000..91f3005717 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/bbs_election_metron_notifier.go @@ -0,0 +1,42 @@ +package metrics + +import ( + "os" + + "github.com/tedsuo/ifrit" + + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +const bbsMasterElectedMetric = "BBSMasterElected" + +type BBSElectionMetronNotifier struct { + Logger lager.Logger + metronClient loggingclient.IngressClient +} + +func NewBBSElectionMetronNotifier(logger lager.Logger, metronClient loggingclient.IngressClient) ifrit.Runner { + return &BBSElectionMetronNotifier{ + Logger: logger, + metronClient: metronClient, + } +} + +func (notifier BBSElectionMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.Logger.Session("metrics-notifier") + logger.Info("starting") + + close(ready) + + logger.Info("started") + defer logger.Info("finished") + + err := notifier.metronClient.SendMetric(bbsMasterElectedMetric, 1) + if err != nil { + logger.Debug("failed-to-emit-bbs-master-elected-metric", lager.Data{"error": err}) + } + + <-signals + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/db_stat_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/db_stat_metron_notifier.go new file mode 100644 index 0000000000..2853b7be88 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/db_stat_metron_notifier.go @@ -0,0 +1,120 @@ +package metrics + +import ( + "os" + "time" + + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers/monitor" + logging "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "github.com/tedsuo/ifrit" +) + +const ( + DefaultEmitFrequency = 60 * time.Second + + dbOpenConnectionsMetric = "DBOpenConnections" + dbWaitDurationMetric = "DBWaitDuration" + dbWaitCountMetric = "DBWaitCount" + dbQueriesTotalMetric = "DBQueriesTotal" + dbQueriesSucceededMetric = "DBQueriesSucceeded" + dbQueriesFailedMetric = "DBQueriesFailed" + dbQueriesInFlightMetric = "DBQueriesInFlight" + dbQueryDurationMaxMetric = "DBQueryDurationMax" +) + +//go:generate counterfeiter -generate + +//counterfeiter:generate . DBStats +type DBStats interface { + OpenConnections() int + WaitDuration() time.Duration + WaitCount() int64 +} + +type dbStatMetronNotifier struct { + logger lager.Logger + clock clock.Clock + dbStats DBStats + metronClient logging.IngressClient + monitor monitor.Monitor +} + +func NewDBStatMetronNotifier(logger lager.Logger, clock clock.Clock, dbStats DBStats, metronClient logging.IngressClient, monitor monitor.Monitor) ifrit.Runner { + return &dbStatMetronNotifier{ + logger: logger, + clock: clock, + dbStats: dbStats, + metronClient: metronClient, + monitor: monitor, + } +} + +func (notifier *dbStatMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.logger.Session("db-stat-metron-notifier") + logger.Info("starting", lager.Data{"interval": DefaultEmitFrequency}) + defer logger.Info("completed") + + ticker := notifier.clock.NewTicker(DefaultEmitFrequency) + close(ready) + + for { + select { + case <-signals: + return nil + case <-ticker.C(): + logger.Debug("emitting-metrics") + + openConnections := notifier.dbStats.OpenConnections() + err := notifier.metronClient.SendMetric(dbOpenConnectionsMetric, openConnections) + if err != nil { + logger.Error("failed-sending-db-open-connections-count", err) + } + + waitDuration := notifier.dbStats.WaitDuration() + err = notifier.metronClient.SendDuration(dbWaitDurationMetric, waitDuration) + if err != nil { + logger.Error("failed-sending-db-wait-duration", err) + } + + waitCount := notifier.dbStats.WaitCount() + err = notifier.metronClient.SendMetric(dbWaitCountMetric, int(waitCount)) + if err != nil { + logger.Error("failed-sending-db-wait-count", err) + } + + total := notifier.monitor.Total() + err = notifier.metronClient.SendMetric(dbQueriesTotalMetric, int(total)) + if err != nil { + logger.Error("failed-sending-db-queries-total-count", err) + } + + succeeded := notifier.monitor.Succeeded() + err = notifier.metronClient.SendMetric(dbQueriesSucceededMetric, int(succeeded)) + if err != nil { + logger.Error("failed-sending-db-queries-succeeded-count", err) + } + + failed := notifier.monitor.Failed() + err = notifier.metronClient.SendMetric(dbQueriesFailedMetric, int(failed)) + if err != nil { + logger.Error("failed-sending-db-queries-failed-count", err) + } + + inFlightMax := notifier.monitor.ReadAndResetInFlightMax() + err = notifier.metronClient.SendMetric(dbQueriesInFlightMetric, int(inFlightMax)) + if err != nil { + logger.Error("failed-sending-db-queries-in-flight-count", err) + } + + durationMax := notifier.monitor.ReadAndResetDurationMax() + err = notifier.metronClient.SendDuration(dbQueryDurationMaxMetric, durationMax) + if err != nil { + logger.Error("failed-sending-db-query-duration-max", err) + } + + logger.Debug("done-emitting-metrics") + } + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/file_descriptor_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/file_descriptor_metron_notifier.go new file mode 100644 index 0000000000..26e20a71e7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/file_descriptor_metron_notifier.go @@ -0,0 +1,76 @@ +package metrics + +import ( + "os" + + "github.com/tedsuo/ifrit" + + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +const ( + OpenFileDescriptorsMetric = "OpenFileDescriptors" + FileDescriptorUnits = "descriptors" +) + +type FileDescriptorMetronNotifier struct { + Logger lager.Logger + metronClient loggingclient.IngressClient + ticker clock.Ticker + procFSPath string +} + +func NewFileDescriptorMetronNotifier(logger lager.Logger, newTicker clock.Ticker, metronClient loggingclient.IngressClient, procPath string) ifrit.Runner { + return &FileDescriptorMetronNotifier{ + Logger: logger, + metronClient: metronClient, + ticker: newTicker, + procFSPath: procPath, + } +} + +func (notifier FileDescriptorMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.Logger.Session("file-descriptor-notifier") + logger.Info("starting") + + close(ready) + + logger.Info("started") + defer logger.Info("finished") + + for { + select { + case <-notifier.ticker.C(): + nDescriptors, err := notifier.descriptorCount() + + if err != nil { + logger.Error("failed-to-read-proc-filesystem", err) + continue + } + + err = notifier.metronClient.SendMetric(OpenFileDescriptorsMetric, nDescriptors) + if err != nil { + logger.Error("error-sending-metric", err) + } + case <-signals: + return nil + } + } +} + +func (notifier FileDescriptorMetronNotifier) descriptorCount() (int, error) { + descriptorInfos, err := os.ReadDir(notifier.procFSPath) + + if err != nil { + return 0, err + } + + count := 0 + for range descriptorInfos { + count++ + } + + return count, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/lrp_stat_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/lrp_stat_metron_notifier.go new file mode 100644 index 0000000000..4ffe6c9475 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/lrp_stat_metron_notifier.go @@ -0,0 +1,204 @@ +package metrics + +import ( + "os" + "sync" + "time" + + "code.cloudfoundry.org/clock" + logging "code.cloudfoundry.org/diego-logging-client" + loggregator "code.cloudfoundry.org/go-loggregator/v9" + "code.cloudfoundry.org/lager/v3" + "github.com/tedsuo/ifrit" +) + +const ( + DefaultEmitMetricsFrequency = 15 * time.Second + + domainMetricPrefix = "Domain." + + ConvergenceLRPRunsMetric = "ConvergenceLRPRuns" + ConvergenceLRPDurationMetric = "ConvergenceLRPDuration" + + LRPsUnclaimedMetric = "LRPsUnclaimed" + LRPsClaimedMetric = "LRPsClaimed" + LRPsRunningMetric = "LRPsRunning" + CrashedActualLRPsMetric = "CrashedActualLRPs" + LRPsMissingMetric = "LRPsMissing" + LRPsExtraMetric = "LRPsExtra" + + SuspectRunningLRPsMetric = "SuspectRunningActualLRPs" + SuspectClaimedLRPsMetric = "SuspectClaimedActualLRPs" + + LRPsDesiredMetric = "LRPsDesired" + CrashingDesiredLRPsMetric = "CrashingDesiredLRPs" + + PresentCellsMetric = "PresentCells" + SuspectCellsMetric = "SuspectCells" +) + +//counterfeiter:generate -o fakes/fake_lrp_stat_metron_notifier.go . LRPStatMetronNotifier +type LRPStatMetronNotifier interface { + ifrit.Runner + + RecordFreshDomains(domains []string) + RecordConvergenceDuration(duration time.Duration) + RecordLRPCounts( + unclaimed, claimed, running, crashed, missing, extra, + suspectRunning, suspectClaimed, desired, crashingDesired int, + ) + RecordCellCounts(present, suspect int) +} + +type lrpStatMetronNotifier struct { + clock clock.Clock + mutex sync.Mutex + metricSender loggingMetricSender + + metrics lrpMetrics +} + +type lrpMetrics struct { + domainsMetric []string + + convergenceLRPRuns uint64 + convergenceLRPDuration time.Duration + + lrpsUnclaimed int + lrpsClaimed int + lrpsRunning int + crashedActualLRPs int + lrpsMissing int + lrpsExtra int + + suspectRunningLRPs int + suspectClaimedLRPs int + + lrpsDesired int + crashingDesiredLRPs int + + presentCells int + suspectCells int +} + +func NewLRPStatMetronNotifier(logger lager.Logger, clock clock.Clock, metronClient logging.IngressClient) LRPStatMetronNotifier { + return &lrpStatMetronNotifier{ + clock: clock, + metricSender: loggingMetricSender{ + logger: logger, + metronClient: metronClient, + }, + } +} + +func (t *lrpStatMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + ticker := t.clock.NewTicker(DefaultEmitMetricsFrequency) + close(ready) + for { + select { + case <-ticker.C(): + t.emitMetrics() + case <-signals: + return nil + } + } +} + +func (lrp *lrpStatMetronNotifier) RecordFreshDomains(domains []string) { + lrp.mutex.Lock() + defer lrp.mutex.Unlock() + + lrp.metrics.domainsMetric = domains +} + +func (lrp *lrpStatMetronNotifier) RecordConvergenceDuration(duration time.Duration) { + lrp.mutex.Lock() + defer lrp.mutex.Unlock() + + lrp.metrics.convergenceLRPRuns++ + lrp.metrics.convergenceLRPDuration = duration +} + +func (lrp *lrpStatMetronNotifier) RecordLRPCounts( + unclaimed, claimed, running, crashed, missing, extra, + suspectRunning, suspectClaimed, desired, crashingDesired int, +) { + lrp.mutex.Lock() + defer lrp.mutex.Unlock() + + lrp.metrics.lrpsUnclaimed = unclaimed + lrp.metrics.lrpsClaimed = claimed + lrp.metrics.lrpsRunning = running + lrp.metrics.crashedActualLRPs = crashed + lrp.metrics.lrpsMissing = missing + lrp.metrics.lrpsExtra = extra + + lrp.metrics.suspectRunningLRPs = suspectRunning + lrp.metrics.suspectClaimedLRPs = suspectClaimed + + lrp.metrics.lrpsDesired = desired + lrp.metrics.crashingDesiredLRPs = crashingDesired +} + +func (lrp *lrpStatMetronNotifier) RecordCellCounts(present int, suspect int) { + lrp.mutex.Lock() + defer lrp.mutex.Unlock() + + lrp.metrics.presentCells = present + lrp.metrics.suspectCells = suspect +} + +func (lrp *lrpStatMetronNotifier) emitMetrics() { + lrp.mutex.Lock() + defer lrp.mutex.Unlock() + + if lrp.metrics.convergenceLRPRuns > 0 { + lrp.metricSender.IncrementCounterWithDelta(ConvergenceLRPRunsMetric, lrp.metrics.convergenceLRPRuns) + lrp.metrics.convergenceLRPRuns = 0 + } + lrp.metricSender.SendDuration(ConvergenceLRPDurationMetric, lrp.metrics.convergenceLRPDuration) + + for _, domain := range lrp.metrics.domainsMetric { + lrp.metricSender.SendMetric(domainMetricPrefix+domain, 1) + } + + lrp.metricSender.SendMetric(LRPsUnclaimedMetric, lrp.metrics.lrpsUnclaimed) + lrp.metricSender.SendMetric(LRPsClaimedMetric, lrp.metrics.lrpsClaimed) + lrp.metricSender.SendMetric(LRPsRunningMetric, lrp.metrics.lrpsRunning) + lrp.metricSender.SendMetric(CrashedActualLRPsMetric, lrp.metrics.crashedActualLRPs) + lrp.metricSender.SendMetric(LRPsMissingMetric, lrp.metrics.lrpsMissing) + lrp.metricSender.SendMetric(LRPsExtraMetric, lrp.metrics.lrpsExtra) + lrp.metricSender.SendMetric(SuspectRunningLRPsMetric, lrp.metrics.suspectRunningLRPs) + lrp.metricSender.SendMetric(SuspectClaimedLRPsMetric, lrp.metrics.suspectClaimedLRPs) + lrp.metricSender.SendMetric(LRPsDesiredMetric, lrp.metrics.lrpsDesired) + lrp.metricSender.SendMetric(CrashingDesiredLRPsMetric, lrp.metrics.crashingDesiredLRPs) + + lrp.metricSender.SendMetric(PresentCellsMetric, lrp.metrics.presentCells) + lrp.metricSender.SendMetric(SuspectCellsMetric, lrp.metrics.suspectCells) +} + +type loggingMetricSender struct { + logger lager.Logger + metronClient logging.IngressClient +} + +func (l loggingMetricSender) logMetricErr(metricName string, err error) { + if err != nil { + l.logger.Error("failed-sending-metric", err, lager.Data{"metric-name": metricName}) + } +} + +func (l loggingMetricSender) SendMetric(name string, value int, opts ...loggregator.EmitGaugeOption) { + f := l.metronClient.SendMetric + l.logMetricErr(name, f(name, value, opts...)) +} + +func (l loggingMetricSender) SendDuration(name string, value time.Duration, opts ...loggregator.EmitGaugeOption) { + f := l.metronClient.SendDuration + l.logMetricErr(name, f(name, value, opts...)) +} + +func (l loggingMetricSender) IncrementCounterWithDelta(name string, value uint64) { + f := l.metronClient.IncrementCounterWithDelta + l.logMetricErr(name, f(name, value)) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/package.go new file mode 100644 index 0000000000..81c4bc5cd8 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/package.go @@ -0,0 +1 @@ +package metrics // import "code.cloudfoundry.org/bbs/metrics" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/request_stat_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/request_stat_metron_notifier.go new file mode 100644 index 0000000000..09b88fe2b4 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/request_stat_metron_notifier.go @@ -0,0 +1,172 @@ +package metrics + +import ( + "os" + "sync" + "time" + + "code.cloudfoundry.org/bbs/cmd/bbs/config" + + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +const ( + requestCounter = "RequestCount" + requestLatencyDuration = "RequestLatency" +) + +type requestMetrics struct { + requestCount uint64 + maxRequestLatency time.Duration +} + +type RequestStatMetronNotifier struct { + logger lager.Logger + ticker clock.Ticker + requestMetricsAll requestMetrics + requestMetricsPerRoute map[string]*requestMetrics + lock sync.Mutex + metronClient loggingclient.IngressClient + advancedMetricsConfig config.AdvancedMetrics +} + +func NewRequestStatMetronNotifier( + logger lager.Logger, + ticker clock.Ticker, + metronClient loggingclient.IngressClient, + advancedMetricsConfig config.AdvancedMetrics) *RequestStatMetronNotifier { + + requestMetricsPerRoute := make(map[string]*requestMetrics) + + if advancedMetricsConfig.Enabled { + initRoutes(advancedMetricsConfig.RouteConfig, requestMetricsPerRoute) + } + + return &RequestStatMetronNotifier{ + logger: logger, + ticker: ticker, + metronClient: metronClient, + requestMetricsPerRoute: requestMetricsPerRoute, + advancedMetricsConfig: advancedMetricsConfig, + } +} + +func initRoutes(routeConfig config.RouteConfiguration, requestMetricsPerRoute map[string]*requestMetrics) { + initRouteMaps := func(routes []string) { + for _, route := range routes { + requestMetricsPerRoute[route] = &requestMetrics{} + } + } + + initRouteMaps(routeConfig.RequestCountRoutes) + initRouteMaps(routeConfig.RequestLatencyRoutes) +} + +func (notifier *RequestStatMetronNotifier) IncrementRequestCounter(delta int, route string) { + notifier.lock.Lock() + defer notifier.lock.Unlock() + + if route != "" { + notifier.requestMetricsPerRoute[route].requestCount += uint64(delta) + + return + } + + notifier.requestMetricsAll.requestCount += uint64(delta) +} + +func (notifier *RequestStatMetronNotifier) UpdateLatency(latency time.Duration, route string) { + notifier.lock.Lock() + defer notifier.lock.Unlock() + + updateLatency := func(metrics *requestMetrics) { + if latency > metrics.maxRequestLatency { + metrics.maxRequestLatency = latency + } + } + + if route != "" { + updateLatency(notifier.requestMetricsPerRoute[route]) + + return + } + + updateLatency(¬ifier.requestMetricsAll) +} + +func readAndResetMetric[MetricType uint64 | time.Duration](metric *MetricType) MetricType { + currentMetricValue := *metric + *metric = 0 + + return currentMetricValue +} + +func (notifier *RequestStatMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.logger.Session("periodic-count-metrics-notifier") + close(ready) + + logger.Info("started") + defer logger.Info("finished") + + for { + select { + case <-notifier.ticker.C(): + notifier.emitMetrics(logger) + case <-signals: + return nil + } + } +} + +func (notifier *RequestStatMetronNotifier) emitMetrics(logger lager.Logger) { + notifier.lock.Lock() + defer notifier.lock.Unlock() + + // Emit Default Metrics + requestCountMetricValue := readAndResetMetric(¬ifier.requestMetricsAll.requestCount) + notifier.emitRequestCount("", requestCountMetricValue, logger) + + requestLatencyMetricValue := readAndResetMetric(¬ifier.requestMetricsAll.maxRequestLatency) + notifier.emitRequestLatency("", requestLatencyMetricValue, logger) + + // Emit Route Specific/Advanced Metrics + if !notifier.advancedMetricsConfig.Enabled { + return + } + + for _, route := range notifier.advancedMetricsConfig.RouteConfig.RequestCountRoutes { + requestCountMetricValue := readAndResetMetric(¬ifier.requestMetricsPerRoute[route].requestCount) + notifier.emitRequestCount("."+route, requestCountMetricValue, logger) + } + + for _, route := range notifier.advancedMetricsConfig.RouteConfig.RequestLatencyRoutes { + requestLatencyMetricValue := readAndResetMetric(¬ifier.requestMetricsPerRoute[route].maxRequestLatency) + notifier.emitRequestLatency("."+route, requestLatencyMetricValue, logger) + } +} + +func (notifier *RequestStatMetronNotifier) emitRequestLatency( + postfix string, + requestLatencyMetricValue time.Duration, + logger lager.Logger) { + + logger.Info("sending-latency", lager.Data{"latency": requestLatencyMetricValue}) + metricErr := notifier.metronClient.SendDuration(requestLatencyDuration+postfix, requestLatencyMetricValue) + if metricErr != nil { + logger.Debug("failed-to-emit-request-latency-metric", lager.Data{"error": metricErr}) + } +} + +func (notifier *RequestStatMetronNotifier) emitRequestCount( + postfix string, + requestCountMetricValue uint64, + logger lager.Logger) { + + logger.Info("adding-counter", lager.Data{"add": requestCountMetricValue}) + metricErr := notifier.metronClient.IncrementCounterWithDelta(requestCounter+postfix, requestCountMetricValue) + if metricErr != nil { + logger.Debug("failed-to-emit-request-counter", lager.Data{"error": metricErr}) + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/task_stat_metron_notifier.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/task_stat_metron_notifier.go new file mode 100644 index 0000000000..00eb9cc680 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/metrics/task_stat_metron_notifier.go @@ -0,0 +1,161 @@ +package metrics + +import ( + "os" + "sync" + "time" + + "code.cloudfoundry.org/clock" + logging "code.cloudfoundry.org/diego-logging-client" + loggregator "code.cloudfoundry.org/go-loggregator/v9" + "code.cloudfoundry.org/lager/v3" + "github.com/tedsuo/ifrit" +) + +const ( + DefaultTaskEmitMetricsFrequency = 15 * time.Second + + ConvergenceTaskRunsMetric = "ConvergenceTaskRuns" + ConvergenceTaskDurationMetric = "ConvergenceTaskDuration" + + TasksStartedMetric = "TasksStarted" + TasksSucceededMetric = "TasksSucceeded" + TasksFailedMetric = "TasksFailed" + + TasksPendingMetric = "TasksPending" + TasksRunningMetric = "TasksRunning" + TasksCompletedMetric = "TasksCompleted" + TasksResolvingMetric = "TasksResolving" + + ConvergenceTasksPrunedMetric = "ConvergenceTasksPruned" + ConvergenceTasksKickedMetric = "ConvergenceTasksKicked" +) + +//counterfeiter:generate -o fakes/fake_task_stat_metron_notifier.go . TaskStatMetronNotifier +type TaskStatMetronNotifier interface { + ifrit.Runner + + RecordConvergenceDuration(duration time.Duration) + RecordTaskStarted(cellID string) + RecordTaskSucceeded(cellID string) + RecordTaskFailed(cellID string) + RecordTaskCounts(pending, running, completed, resolved int, pruned, kicked uint64) +} + +type taskStatMetronNotifier struct { + mutex sync.Mutex + clock clock.Clock + metricSender loggingMetricSender + + perCellMetrics map[string]perCellMetrics + globalMetrics globalMetrics +} + +type perCellMetrics struct { + tasksStarted, tasksFailed, tasksSucceeded int +} + +type globalMetrics struct { + convergenceTaskRuns uint64 + convergenceTaskDuration time.Duration + + tasksPending, tasksRunning, tasksCompleted, tasksResolving int + convergenceTasksPruned, convergenceTasksKicked uint64 +} + +func NewTaskStatMetronNotifier(logger lager.Logger, clock clock.Clock, metronClient logging.IngressClient) TaskStatMetronNotifier { + return &taskStatMetronNotifier{ + clock: clock, + metricSender: loggingMetricSender{ + logger: logger, + metronClient: metronClient, + }, + perCellMetrics: make(map[string]perCellMetrics), + } +} + +func (t *taskStatMetronNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + ticker := t.clock.NewTicker(DefaultEmitMetricsFrequency) + close(ready) + for { + select { + case <-ticker.C(): + t.emitMetrics() + case <-signals: + return nil + } + } +} + +func (t *taskStatMetronNotifier) RecordConvergenceDuration(duration time.Duration) { + t.mutex.Lock() + defer t.mutex.Unlock() + + t.globalMetrics.convergenceTaskRuns += 1 + t.globalMetrics.convergenceTaskDuration = duration +} + +func (t *taskStatMetronNotifier) RecordTaskStarted(cellID string) { + t.mutex.Lock() + defer t.mutex.Unlock() + + stats := t.perCellMetrics[cellID] + stats.tasksStarted += 1 + t.perCellMetrics[cellID] = stats +} + +func (t *taskStatMetronNotifier) RecordTaskSucceeded(cellID string) { + t.mutex.Lock() + defer t.mutex.Unlock() + + stats := t.perCellMetrics[cellID] + stats.tasksSucceeded += 1 + t.perCellMetrics[cellID] = stats +} + +func (t *taskStatMetronNotifier) RecordTaskFailed(cellID string) { + t.mutex.Lock() + defer t.mutex.Unlock() + + stats := t.perCellMetrics[cellID] + stats.tasksFailed += 1 + t.perCellMetrics[cellID] = stats +} + +func (t *taskStatMetronNotifier) RecordTaskCounts(pending, running, completed, resolving int, pruned, kicked uint64) { + t.mutex.Lock() + defer t.mutex.Unlock() + + t.globalMetrics.tasksPending = pending + t.globalMetrics.tasksRunning = running + t.globalMetrics.tasksCompleted = completed + t.globalMetrics.tasksResolving = resolving + t.globalMetrics.convergenceTasksPruned = pruned + t.globalMetrics.convergenceTasksKicked = kicked +} + +func (t *taskStatMetronNotifier) emitMetrics() { + t.mutex.Lock() + defer t.mutex.Unlock() + + for cell, stats := range t.perCellMetrics { + opt := loggregator.WithEnvelopeTag("cell-id", cell) + t.metricSender.SendMetric(TasksStartedMetric, stats.tasksStarted, opt) + t.metricSender.SendMetric(TasksFailedMetric, stats.tasksFailed, opt) + t.metricSender.SendMetric(TasksSucceededMetric, stats.tasksSucceeded, opt) + } + + t.metricSender.SendMetric(TasksPendingMetric, t.globalMetrics.tasksPending) + t.metricSender.SendMetric(TasksRunningMetric, t.globalMetrics.tasksRunning) + t.metricSender.SendMetric(TasksCompletedMetric, t.globalMetrics.tasksCompleted) + t.metricSender.SendMetric(TasksResolvingMetric, t.globalMetrics.tasksResolving) + t.metricSender.IncrementCounterWithDelta(ConvergenceTasksPrunedMetric, t.globalMetrics.convergenceTasksPruned) + t.metricSender.IncrementCounterWithDelta(ConvergenceTasksKickedMetric, t.globalMetrics.convergenceTasksKicked) + + if t.globalMetrics.convergenceTaskRuns > 0 { + t.metricSender.IncrementCounterWithDelta(ConvergenceTaskRunsMetric, t.globalMetrics.convergenceTaskRuns) + t.globalMetrics.convergenceTaskRuns = 0 + } + + t.metricSender.SendDuration(ConvergenceTaskDurationMetric, t.globalMetrics.convergenceTaskDuration) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/manager.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/manager.go new file mode 100644 index 0000000000..be7e859765 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/manager.go @@ -0,0 +1,250 @@ +package migration + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "sort" + "time" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/bbs/models" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" +) + +const ( + migrationDuration = "MigrationDuration" +) + +type Manager struct { + logger lager.Logger + sqlDB db.DB + rawSQLDB *sql.DB + cryptor encryption.Cryptor + migrations []Migration + migrationsDone chan<- struct{} + clock clock.Clock + databaseDriver string + metronClient loggingclient.IngressClient +} + +func NewManager( + logger lager.Logger, + sqlDB db.DB, + rawSQLDB *sql.DB, + cryptor encryption.Cryptor, + migrations Migrations, + migrationsDone chan<- struct{}, + clock clock.Clock, + databaseDriver string, + metronClient loggingclient.IngressClient, +) Manager { + sort.Sort(migrations) + + return Manager{ + logger: logger, + sqlDB: sqlDB, + rawSQLDB: rawSQLDB, + cryptor: cryptor, + migrations: migrations, + migrationsDone: migrationsDone, + clock: clock, + databaseDriver: databaseDriver, + metronClient: metronClient, + } +} + +func (m Manager) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := m.logger.Session("migration-manager") + logger.Info("starting") + + if m.rawSQLDB == nil { + err := errors.New("no database configured") + logger.Error("no-database-configured", err) + return err + } + + var maxMigrationVersion int64 + if len(m.migrations) > 0 { + maxMigrationVersion = m.migrations[len(m.migrations)-1].Version() + } + version, err := m.initializeVersion(logger) + if err != nil { + return err + } + + if version > maxMigrationVersion { + return fmt.Errorf( + "existing DB version (%d) exceeds bbs version (%d)", + version, + maxMigrationVersion, + ) + } + + errorChan := make(chan error) + go m.performMigration(logger, version, maxMigrationVersion, errorChan, ready) + defer logger.Info("exited") + + select { + case err := <-errorChan: + logger.Error("migration-failed", err) + return err + case <-signals: + logger.Info("migration-interrupt") + return nil + } +} + +func (m *Manager) performMigration( + logger lager.Logger, + version int64, + maxMigrationVersion int64, + errorChan chan error, + readyChan chan<- struct{}, +) { + migrateStart := m.clock.Now() + if version != maxMigrationVersion { + lastVersion := version + + for _, currentMigration := range m.migrations { + if maxMigrationVersion < currentMigration.Version() { + break + } + + if lastVersion < currentMigration.Version() { + nextVersion := currentMigration.Version() + logger.Info("running-migration", lager.Data{ + "current_version": lastVersion, + "migration_version": nextVersion, + }) + + tx, err := m.rawSQLDB.Begin() + if err != nil { + errorChan <- err + return + } + defer tx.Rollback() + + currentMigration.SetCryptor(m.cryptor) + currentMigration.SetClock(m.clock) + currentMigration.SetDBFlavor(m.databaseDriver) + + err = currentMigration.Up(tx, m.logger.Session("migration")) + if err != nil { + errorChan <- err + return + } + + lastVersion = nextVersion + + err = m.writeVersion(tx, lastVersion) + if err != nil { + errorChan <- err + return + } + + err = tx.Commit() + if err != nil { + errorChan <- err + return + } + + logger.Info("completed-migration", lager.Data{ + "current_version": lastVersion, + "target_version": maxMigrationVersion, + }) + } + } + } + + logger.Debug("migrations-finished") + + err := m.metronClient.SendDuration(migrationDuration, time.Since(migrateStart)) + if err != nil { + logger.Error("failed-to-send-migration-duration-metric", err) + } + + m.finish(logger, readyChan) +} + +func (m Manager) initializeVersion(logger lager.Logger) (int64, error) { + tx, err := m.rawSQLDB.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + version, err := m.sqlDB.Version(&queryableTx{tx}, context.Background(), logger) + if err == nil { + return version.CurrentVersion, nil + } + + if err != models.ErrResourceNotFound { + return -1, err + } + + err = m.writeVersion(tx, 0) + if err != nil { + return -1, err + } + + err = tx.Commit() + if err != nil { + return -1, err + } + + return 0, nil +} + +func (m *Manager) finish(logger lager.Logger, ready chan<- struct{}) { + close(ready) + close(m.migrationsDone) + logger.Info("finished-migrations") +} + +func (m *Manager) writeVersion(tx *sql.Tx, currentVersion int64) error { + return m.sqlDB.SetVersion(&queryableTx{tx}, context.Background(), m.logger, &models.Version{ + CurrentVersion: currentVersion, + }) +} + +type Migrations []Migration + +func (m Migrations) Len() int { return len(m) } +func (m Migrations) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m Migrations) Less(i, j int) bool { return m[i].Version() < m[j].Version() } + +type queryableTx struct { + tx *sql.Tx +} + +func (tx *queryableTx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + return tx.tx.ExecContext(ctx, query, args...) +} + +func (tx *queryableTx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + return tx.tx.PrepareContext(ctx, query) +} + +func (tx *queryableTx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + return tx.tx.QueryContext(ctx, query, args...) +} + +func (tx *queryableTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) helpers.RowScanner { + // meow - perhaps this is where the nil exception is? does tx.tx not exist? + return tx.tx.QueryRowContext(ctx, query, args...) +} + +func (tx *queryableTx) Commit() error { + return tx.tx.Commit() +} + +func (tx *queryableTx) Rollback() error { + return tx.tx.Rollback() +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/migration.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/migration.go new file mode 100644 index 0000000000..07472d9dab --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/migration.go @@ -0,0 +1,22 @@ +package migration + +import ( + "database/sql" + + "code.cloudfoundry.org/bbs/encryption" + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" +) + +//go:generate counterfeiter -generate + +//counterfeiter:generate -o migrationfakes/fake_migration.go . Migration + +type Migration interface { + String() string + Version() int64 + Up(tx *sql.Tx, logger lager.Logger) error + SetCryptor(cryptor encryption.Cryptor) + SetClock(c clock.Clock) + SetDBFlavor(flavor string) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/package.go new file mode 100644 index 0000000000..619cb55de5 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/migration/package.go @@ -0,0 +1 @@ +package migration // import "code.cloudfoundry.org/bbs/migration" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/package.go new file mode 100644 index 0000000000..a766f5a9d7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/package.go @@ -0,0 +1 @@ +package taskworkpool // import "code.cloudfoundry.org/bbs/taskworkpool" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/taskcallback.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/taskcallback.go new file mode 100644 index 0000000000..c63fbeb957 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/bbs/taskworkpool/taskcallback.go @@ -0,0 +1,158 @@ +package taskworkpool + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "net/http" + "os" + "regexp" + "time" + + "code.cloudfoundry.org/bbs/db" + "code.cloudfoundry.org/bbs/events" + "code.cloudfoundry.org/bbs/models" + cfhttp "code.cloudfoundry.org/cfhttp/v2" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/workpool" +) + +const MAX_CB_RETRIES = 3 + +//go:generate counterfeiter -generate + +//counterfeiter:generate . TaskCompletionClient + +type CompletedTaskHandler func(logger lager.Logger, httpClient *http.Client, taskDB db.TaskDB, taskHub events.Hub, task *models.Task) + +type TaskCompletionClient interface { + Submit(taskDB db.TaskDB, taskHub events.Hub, task *models.Task) +} + +type TaskCompletionWorkPool struct { + logger lager.Logger + maxWorkers int + callbackHandler CompletedTaskHandler + callbackWorkPool *workpool.WorkPool + httpClient *http.Client +} + +func New(logger lager.Logger, maxWorkers int, cbHandler CompletedTaskHandler, tlsConfig *tls.Config, requestTimeout time.Duration) *TaskCompletionWorkPool { + if cbHandler == nil { + panic("callbackHandler cannot be nil") + } + + httpClient := cfhttp.NewClient( + cfhttp.WithTLSConfig(tlsConfig), + cfhttp.WithRequestTimeout(requestTimeout), + ) + + return &TaskCompletionWorkPool{ + logger: logger.Session("task-completion-workpool"), + maxWorkers: maxWorkers, + callbackHandler: cbHandler, + httpClient: httpClient, + } +} + +func (twp *TaskCompletionWorkPool) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + cbWorkPool, err := workpool.NewWorkPool(twp.maxWorkers) + logger := twp.logger + logger.Info("starting") + + if err != nil { + logger.Error("creation-failed", err) + return err + } + twp.callbackWorkPool = cbWorkPool + close(ready) + logger.Info("started") + defer logger.Info("finished") + + <-signals + twp.callbackWorkPool.Stop() + + return nil +} + +func (twp *TaskCompletionWorkPool) Submit(taskDB db.TaskDB, taskHub events.Hub, task *models.Task) { + if twp.callbackWorkPool == nil { + panic("called submit before workpool was started") + } + logger := twp.logger + twp.callbackWorkPool.Submit(func() { + twp.callbackHandler(logger, twp.httpClient, taskDB, taskHub, task) + }) +} + +func HandleCompletedTask(logger lager.Logger, httpClient *http.Client, taskDB db.TaskDB, taskHub events.Hub, task *models.Task) { + logger = logger.Session("handle-completed-task", lager.Data{"task_guid": task.TaskGuid}) + + if task.CompletionCallbackUrl != "" { + before, after, modelErr := taskDB.ResolvingTask(context.Background(), logger, task.TaskGuid) + if modelErr != nil { + logger.Error("marking-task-as-resolving-failed", modelErr) + return + } + go taskHub.Emit(models.NewTaskChangedEvent(before, after)) + + logger = logger.WithData(lager.Data{"callback_url": task.CompletionCallbackUrl}) + + json, err := json.Marshal(&models.TaskCallbackResponse{ + TaskGuid: task.TaskGuid, + Failed: task.Failed, + FailureReason: task.FailureReason, + Result: task.Result, + Annotation: task.Annotation, + CreatedAt: task.CreatedAt, + }) + if err != nil { + logger.Error("marshalling-task-failed", err) + return + } + + var statusCode int + + retriableErrRegexp := regexp.MustCompile("Client.Timeout|use of closed network connection") + for i := 0; i < MAX_CB_RETRIES; i++ { + request, err := http.NewRequest("POST", task.CompletionCallbackUrl, bytes.NewReader(json)) + if err != nil { + logger.Error("building-request-failed", err) + return + } + + request.Header.Set("Content-Type", "application/json") + response, err := httpClient.Do(request) + if err != nil { + if retriableErrRegexp.MatchString(err.Error()) { + continue + } + logger.Error("doing-request-failed", err) + return + } + defer response.Body.Close() + + statusCode = response.StatusCode + if shouldResolve(statusCode) { + deletedTask, modelErr := taskDB.DeleteTask(context.Background(), logger, task.TaskGuid) + if modelErr != nil { + logger.Error("delete-task-failed", modelErr) + } + go taskHub.Emit(models.NewTaskRemovedEvent(deletedTask)) + return + } + } + + logger.Info("callback-failed", lager.Data{"status_code": statusCode}) + } +} + +func shouldResolve(status int) bool { + switch status { + case http.StatusServiceUnavailable, http.StatusGatewayTimeout: + return false + default: + return true + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/LICENSE b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/NOTICE b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/NOTICE new file mode 100644 index 0000000000..3c8dd5b60e --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/NOTICE @@ -0,0 +1,20 @@ +Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. + +This project contains software that is Copyright (c) 2014-2015 Pivotal Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This project may include a number of subcomponents with separate +copyright notices and license terms. Your use of these subcomponents +is subject to the terms and conditions of each subcomponent's license, +as noted in the LICENSE file. diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/databaseuri/databaseuri.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/databaseuri/databaseuri.go new file mode 100644 index 0000000000..3a792f6539 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/buildpackapplifecycle/databaseuri/databaseuri.go @@ -0,0 +1,54 @@ +package databaseuri + +import ( + "encoding/json" + "net/url" +) + +type Databaseuri struct { +} + +func New() *Databaseuri { + return &Databaseuri{} +} + +func (d *Databaseuri) Credentials(services []byte) ([]string, error) { + data := map[string][]struct { + Credentials struct { + Uri string `json:"uri"` + } `json:"credentials"` + }{} + if err := json.Unmarshal(services, &data); err != nil { + return nil, err + } + + var creds []string + for _, v1 := range data { + for _, v2 := range v1 { + if v2.Credentials.Uri != "" { + creds = append(creds, v2.Credentials.Uri) + } + } + } + return creds, nil +} + +func (d *Databaseuri) Uri(service_uris []string) string { + schemes := map[string]string{ + "mysql": "mysql2", + "mysql2": "", + "postgres": "", + "postgresql": "postgres", + } + for _, service_uri := range service_uris { + if uri, err := url.Parse(service_uri); err == nil { + if val, ok := schemes[uri.Scheme]; ok { + if val != "" { + uri.Scheme = val + } + return uri.String() + } + } + } + return "" +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/guid_provider.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/guid_provider.go new file mode 100644 index 0000000000..2822928bce --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/guid_provider.go @@ -0,0 +1,23 @@ +package guidprovider + +import uuid "github.com/nu7hatch/gouuid" + +//go:generate counterfeiter -generate + +//counterfeiter:generate . GUIDProvider + +type GUIDProvider interface { + NextGUID() (string, error) +} + +var DefaultGuidProvider GUIDProvider = &guidProvider{} + +type guidProvider struct{} + +func (*guidProvider) NextGUID() (string, error) { + guid, err := uuid.NewV4() + if err != nil { + return "", err + } + return guid.String(), nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/package.go new file mode 100644 index 0000000000..af0859ffba --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/diego-db-helpers/guidprovider/package.go @@ -0,0 +1 @@ +package guidprovider // import "code.cloudfoundry.org/diego-db-helpers/guidprovider" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/db_health_check_runner.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/db_health_check_runner.go new file mode 100644 index 0000000000..b60737a5d2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/db_health_check_runner.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "time" + + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/db" +) + +type DBHealthCheckRunner struct { + logger lager.Logger + sqlDB db.LocketHealthCheckDB + clock clock.Clock + lock sync.Mutex + isRunning bool + HealthCheckFailureThreshold int + HealthCheckTimeout time.Duration + HealthCheckInterval time.Duration +} + +func NewDBHealthCheckRunner(logger lager.Logger, sqlDB db.LocketHealthCheckDB, clock clock.Clock, failureCount int, timeout, interval time.Duration) *DBHealthCheckRunner { + if failureCount == 0 { + failureCount = 3 + } + if timeout == 0 { + timeout = 5 * time.Second + } + if interval == 0 { + interval = 10 * time.Second + } + return &DBHealthCheckRunner{ + logger: logger.Session("db-health-check-runner"), + sqlDB: sqlDB, + clock: clock, + HealthCheckFailureThreshold: failureCount, + HealthCheckTimeout: timeout, + HealthCheckInterval: interval, + lock: sync.Mutex{}, + } +} + +func (runner *DBHealthCheckRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + close(ready) + runner.logger.Info("starting") + defer runner.logger.Info("exiting") + + ticker := runner.clock.NewTicker(runner.HealthCheckInterval) + healthCheckResults := make(chan error) + for { + runner.logger.Debug("reentering-run-loop") + select { + case err := <-healthCheckResults: + runner.lock.Lock() + runner.isRunning = false + runner.lock.Unlock() + if err != nil { + runner.logger.Error("database-failure-detected-restarting-locket", err) + return err + } + runner.logger.Info("health-check-succeeded") + case <-signals: + runner.logger.Info("exiting-due-to-signal") + return nil + case <-ticker.C(): + runner.lock.Lock() + if !runner.isRunning { + runner.isRunning = true + runner.logger.Debug("executing-health-check") + go runner.ExecuteTimedHealthCheckWithRetries(healthCheckResults) + } + runner.lock.Unlock() + } + } +} + +func (runner *DBHealthCheckRunner) ExecuteTimedHealthCheckWithRetries(resultChan chan error) { + var errs []error + for i := 1; i <= runner.HealthCheckFailureThreshold; i++ { + logger := runner.logger.WithData(lager.Data{"attempt": i}) + logger.Debug("executing-timed-health-check") + err := runner.ExecuteTimedHealthCheck() + if err != nil { + logger.Error("failed-health-check", err) + errs = append(errs, err) + } else { + resultChan <- nil + return + } + } + finalErr := errors.Join(errs...) + runner.logger.Error("health-check-attempts-exceeded", finalErr, lager.Data{"max-attempts": runner.HealthCheckFailureThreshold}) + resultChan <- finalErr +} + +func (runner *DBHealthCheckRunner) ExecuteTimedHealthCheck() error { + timer := runner.clock.NewTimer(runner.HealthCheckTimeout) + errChan := make(chan error) + go runner.runDBHealthCheck(errChan) + + select { + case err := <-errChan: + if err == nil { + return nil + } else { + return err + } + case <-timer.C(): + err := fmt.Errorf("timed out after %s while executing DB health check", runner.HealthCheckTimeout) + runner.logger.Error("health-check-timed-out", err) + return err + } +} + +func (runner *DBHealthCheckRunner) runDBHealthCheck(errChan chan error) { + err := runner.sqlDB.PerformLocketHealthCheck(context.Background(), runner.logger, time.Now()) + errChan <- err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/main.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/main.go new file mode 100644 index 0000000000..cdb849d7e2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "flag" + "os" + "time" + + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/debugserver" + "code.cloudfoundry.org/diego-db-helpers/guidprovider" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers/monitor" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/go-loggregator/v9/runtimeemitter" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagerflags" + "code.cloudfoundry.org/locket" + "code.cloudfoundry.org/locket/cmd/locket/config" + "code.cloudfoundry.org/locket/db" + "code.cloudfoundry.org/locket/expiration" + "code.cloudfoundry.org/locket/grpcserver" + "code.cloudfoundry.org/locket/handlers" + "code.cloudfoundry.org/locket/metrics" + metrics_helpers "code.cloudfoundry.org/locket/metrics/helpers" + "code.cloudfoundry.org/tlsconfig" + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/grouper" + "github.com/tedsuo/ifrit/sigmon" +) + +var configFilePath = flag.String( + "config", + "", + "Path to Locket JSON Configuration file", +) + +func main() { + flag.Parse() + + cfg, err := config.NewLocketConfig(*configFilePath) + if err != nil { + panic("invalid-config-file: " + err.Error()) + } + + logger, reconfigurableSink := lagerflags.NewFromConfig("locket", cfg.LagerConfig) + + metronClient, err := initializeMetron(logger, cfg) + if err != nil { + logger.Error("failed-to-initialize-metron-client", err) + os.Exit(1) + } + + clock := clock.NewClock() + + dbParams := &helpers.ConnectParams{ + DriverName: cfg.DatabaseDriver, + DatabaseConnectionString: cfg.DatabaseConnectionString, + ConnectionTimeout: time.Duration(cfg.DBConnectionTimeout), + ReadTimeout: time.Duration(cfg.DBReadTimeout), + WriteTimeout: time.Duration(cfg.DBWriteTimeout), + SqlCACertFile: cfg.SQLCACertFile, + SqlEnableIdentityVerification: cfg.SQLEnableIdentityVerification, + } + + sqlConn, err := helpers.Connect( + logger, + dbParams, + ) + + if err != nil { + logger.Fatal("failed-to-open-sql", err) + } + defer sqlConn.Close() + + sqlConn.SetMaxIdleConns(cfg.MaxOpenDatabaseConnections) + sqlConn.SetMaxOpenConns(cfg.MaxOpenDatabaseConnections) + sqlConn.SetConnMaxLifetime(time.Duration(cfg.MaxDatabaseConnectionLifetime)) + + err = sqlConn.Ping() + if err != nil { + logger.Fatal("sql-failed-to-connect", err) + } + + dbMonitor := monitor.New() + monitoredDB := helpers.NewMonitoredDB(sqlConn, dbMonitor) + + sqlDB := db.NewSQLDB( + monitoredDB, + cfg.DatabaseDriver, + guidprovider.DefaultGuidProvider, + ) + + err = sqlDB.CreateLockTable(context.Background(), logger) + if err != nil { + logger.Fatal("failed-to-create-lock-table", err) + } + + if cfg.EnableDBHealthCheck { + err = sqlDB.CreateHealthCheckTable(context.Background(), logger) + if err != nil { + logger.Fatal("failed-to-create-health-check-table", err) + } + } + + tlsConfig, err := tlsconfig.Build( + tlsconfig.WithInternalServiceDefaults(), + tlsconfig.WithIdentityFromFile(cfg.CertFile, cfg.KeyFile), + ).Server(tlsconfig.WithClientAuthenticationFromFile(cfg.CaFile)) + if err != nil { + logger.Fatal("invalid-tls-config", err) + } + + lockMetricsNotifier := metrics.NewLockMetricsNotifier(logger, clock, metronClient, time.Duration(cfg.ReportInterval), sqlDB) + dbMetricsNotifier := metrics.NewDBMetricsNotifier(logger, clock, metronClient, time.Duration(cfg.ReportInterval), sqlDB, dbMonitor) + requestNotifier := metrics_helpers.NewRequestMetricsNotifier(logger, clock, metronClient, time.Duration(cfg.ReportInterval), []string{"Lock", "Release", "Fetch", "FetchAll"}) + lockPick := expiration.NewLockPick(sqlDB, clock, metronClient) + burglar := expiration.NewBurglar(logger, sqlDB, lockPick, clock, locket.RetryInterval, metronClient) + exitCh := make(chan struct{}) + + dbOperationTimeout := handlers.DefaultDBOperationTimeout + if cfg.DBOperationTimeout > 0 { + dbOperationTimeout = time.Duration(cfg.DBOperationTimeout) + } + + handler := handlers.NewLocketHandler(logger, sqlDB, lockPick, requestNotifier, exitCh, dbOperationTimeout) + server := grpcserver.NewGRPCServer(logger, cfg.ListenAddress, tlsConfig, handler) + + var dbHealthCheckRunner ifrit.Runner + if cfg.EnableDBHealthCheck { + dbHealthCheckRunner = NewDBHealthCheckRunner( + logger, + sqlDB, + clock, + cfg.HealthCheckFailureThreshold, + time.Duration(cfg.HealthCheckTimeout), + time.Duration(cfg.HealthCheckInterval), + ) + } + + members := grouper.Members{ + {Name: "server", Runner: server}, + {Name: "burglar", Runner: burglar}, + {Name: "lock-metrics-notifier", Runner: lockMetricsNotifier}, + {Name: "db-metrics-notifier", Runner: dbMetricsNotifier}, + {Name: "request-metrics-notifier", Runner: requestNotifier}, + } + + if cfg.EnableDBHealthCheck { + members = append(grouper.Members{ + {Name: "db-health-check", Runner: dbHealthCheckRunner}, + }, members...) + } + + if cfg.DebugAddress != "" { + members = append(grouper.Members{ + {Name: "debug-server", Runner: debugserver.Runner(cfg.DebugAddress, reconfigurableSink)}, + }, members...) + } + + group := grouper.NewOrdered(os.Interrupt, members) + monitor := ifrit.Invoke(sigmon.New(group)) + + logger.Info("started") + + go func() { + <-exitCh + logger.Info("shutting-down-due-to-unrecoverable-error") + monitor.Signal(os.Interrupt) + }() + + err = <-monitor.Wait() + if err != nil { + logger.Error("exited-with-failure", err) + os.Exit(1) + } +} + +func initializeMetron(logger lager.Logger, locketConfig config.LocketConfig) (loggingclient.IngressClient, error) { + client, err := loggingclient.NewIngressClient(locketConfig.LoggregatorConfig) + if err != nil { + return nil, err + } + + emitter := runtimeemitter.NewV1(client) + go emitter.Run() + + return client, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/package.go new file mode 100644 index 0000000000..58e54b9dd7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/cmd/locket/package.go @@ -0,0 +1 @@ +package main // import "code.cloudfoundry.org/locket/cmd/locket" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/lock_db.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/lock_db.go new file mode 100644 index 0000000000..6f9f3400d4 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/lock_db.go @@ -0,0 +1,311 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/models" +) + +func lagerDataFromLock(resource *models.Resource) lager.Data { + return lager.Data{ + "key": resource.GetKey(), + "owner": resource.GetOwner(), + "type-code": resource.GetTypeCode(), + } +} + +func (db *SQLDB) Lock(ctx context.Context, logger lager.Logger, resource *models.Resource, ttl int64) (*Lock, error) { + logger = logger.Session("lock", lagerDataFromLock(resource)) + var lock *Lock + + var newLock bool + + err := db.helper.Transact(ctx, logger, db, func(logger lager.Logger, tx helpers.Tx) error { + newLock = false + res, index, id, _, err := db.fetchLock(ctx, logger, tx, resource.Key) + if err != nil { + sqlErr := db.helper.ConvertSQLError(err) + if sqlErr != helpers.ErrResourceNotFound { + logger.Error("failed-to-fetch-lock", err) + return err + } + newLock = true + } else if res.Owner != resource.Owner && res.Owner != "" { + logger.Debug("lock-already-exists") + return models.ErrLockCollision + } + + index++ + + modifiedId := id + if modifiedId == "" { + modifiedId, err = db.guidProvider.NextGUID() + if err != nil { + logger.Error("failed-to-generate-guid", err) + return err + } + } + + lock = &Lock{ + Resource: models.GetResource(resource), + ModifiedIndex: index, + ModifiedId: modifiedId, + TtlInSeconds: ttl, + } + + if newLock { + _, err = db.helper.Insert(ctx, logger, tx, "locks", + helpers.SQLAttributes{ + "path": lock.Key, + "owner": lock.Owner, + "value": lock.Value, + "type": lock.Type, + "modified_index": lock.ModifiedIndex, + "modified_id": lock.ModifiedId, + "ttl": lock.TtlInSeconds, + }, + ) + } else { + _, err = db.helper.Update(ctx, logger, tx, "locks", + helpers.SQLAttributes{ + "owner": lock.Owner, + "value": lock.Value, + "type": lock.Type, + "modified_index": lock.ModifiedIndex, + "modified_id": lock.ModifiedId, + "ttl": lock.TtlInSeconds, + }, + "path = ?", lock.Key, + ) + } + + if err != nil { + logger.Error("failed-updating-lock", err) + return err + } + + return nil + }) + + if err == nil && newLock { + logger.Info("acquired-lock") + } + + return lock, db.helper.ConvertSQLError(err) +} + +func (db *SQLDB) Release(ctx context.Context, logger lager.Logger, resource *models.Resource) error { + logger = logger.Session("release-lock", lagerDataFromLock(resource)) + + err := db.helper.Transact(ctx, logger, db, func(logger lager.Logger, tx helpers.Tx) error { + res, _, _, _, err := db.fetchLock(ctx, logger, tx, resource.Key) + if err != nil { + sqlErr := db.helper.ConvertSQLError(err) + if sqlErr == helpers.ErrResourceNotFound { + logger.Debug("lock-does-not-exist") + return nil + } + logger.Error("failed-to-fetch-lock", err) + return sqlErr + } + + if res.Owner != resource.Owner { + logger.Error("cannot-release-lock", models.ErrLockCollision) + return models.ErrLockCollision + } + + _, err = db.helper.Delete(ctx, logger, tx, "locks", + "path = ?", resource.Key, + ) + if err != nil { + logger.Error("failed-to-release-lock", err) + return db.helper.ConvertSQLError(err) + } + logger.Info("released-lock") + return nil + }) + return err +} + +func (db *SQLDB) Fetch(ctx context.Context, logger lager.Logger, key string) (*Lock, error) { + logger = logger.Session("fetch-lock", lager.Data{"key": key}) + var lock *Lock + + err := db.helper.Transact(ctx, logger, db, func(logger lager.Logger, tx helpers.Tx) error { + res, index, id, ttl, err := db.fetchLock(ctx, logger, tx, key) + if err != nil { + logger.Error("failed-to-fetch-lock", err) + sqlErr := db.helper.ConvertSQLError(err) + if sqlErr == helpers.ErrResourceNotFound { + return models.ErrResourceNotFound + } + return sqlErr + } + + if res.Owner == "" { + return models.ErrResourceNotFound + } + + lock = &Lock{Resource: res, ModifiedIndex: index, ModifiedId: id, TtlInSeconds: ttl} + + return nil + }) + + return lock, err +} + +func (db *SQLDB) FetchAll(ctx context.Context, logger lager.Logger, lockType string) ([]*Lock, error) { + logger = logger.Session("fetch-all-locks", lager.Data{"type": lockType}) + var locks []*Lock + + err := db.helper.Transact(ctx, logger, db, func(logger lager.Logger, tx helpers.Tx) error { + var where string + whereBindings := make([]interface{}, 0) + + if lockType != "" { + where = "type = ?" + whereBindings = append(whereBindings, lockType) + } + + rows, err := db.helper.All(ctx, logger, tx, "locks", + helpers.ColumnList{"path", "owner", "value", "type", "modified_index", "modified_id", "ttl"}, + helpers.NoLockRow, where, whereBindings..., + ) + if err != nil { + logger.Error("failed-to-fetch-locks", err) + return err + } + defer rows.Close() + + for rows.Next() { + var key, owner, value, lockType, id string + var index, ttl int64 + + err := rows.Scan(&key, &owner, &value, &lockType, &index, &id, &ttl) + if err != nil { + logger.Error("failed-to-scan-lock", err) + continue + } + + if owner == "" { + continue + } + + locks = append(locks, &Lock{ + Resource: &models.Resource{ + Key: key, + Owner: owner, + Value: value, + Type: lockType, + TypeCode: models.GetTypeCode(lockType), + }, + ModifiedIndex: index, + ModifiedId: id, + TtlInSeconds: ttl, + }) + } + + return nil + }) + + return locks, db.helper.ConvertSQLError(err) +} + +func (db *SQLDB) Count(ctx context.Context, logger lager.Logger, lockType string) (int, error) { + whereBindings := make([]interface{}, 0) + wheres := "owner <> ?" + whereBindings = append(whereBindings, "") + + if lockType != "" { + wheres += " AND type = ?" + whereBindings = append(whereBindings, lockType) + } + + logger = logger.Session("count-locks") + count, err := db.helper.Count(ctx, logger, db, "locks", wheres, whereBindings...) + return count, db.helper.ConvertSQLError(err) +} + +func (db *SQLDB) fetchLock(ctx context.Context, logger lager.Logger, q helpers.Queryable, key string) (*models.Resource, int64, string, int64, error) { + row := db.helper.One(ctx, logger, q, "locks", + helpers.ColumnList{"owner", "value", "type", "modified_index", "modified_id", "ttl"}, + helpers.LockRow, + "path = ?", key, + ) + + var owner, value, lockType, id string + var index, ttl int64 + err := row.Scan(&owner, &value, &lockType, &index, &id, &ttl) + if err != nil { + return nil, 0, "", 0, err + } + + return &models.Resource{ + Key: key, + Owner: owner, + Value: value, + Type: lockType, + TypeCode: models.GetTypeCode(lockType), + }, index, id, ttl, nil +} + +func (db *SQLDB) FetchAndRelease(ctx context.Context, logger lager.Logger, lock *Lock) (bool, error) { + logger = logger.Session("fetch-and-release-lock", lagerDataFromLock(lock.Resource)) + + err := db.helper.Transact(ctx, logger, db, func(logger lager.Logger, tx helpers.Tx) error { + res, index, id, ttl, err := db.fetchLock(ctx, logger, tx, lock.Resource.Key) + + if err != nil { + sqlErr := db.helper.ConvertSQLError(err) + if sqlErr == helpers.ErrResourceNotFound { + logger.Debug("lock-does-not-exist") + return models.ErrResourceNotFound + } + logger.Error("failed-to-fetch-lock", err) + return sqlErr + } + + logger.Info("fetched-lock") + + fetchedLock := &Lock{Resource: res, ModifiedIndex: index, ModifiedId: id, TtlInSeconds: ttl} + + if fetchedLock.Resource.Owner != lock.Resource.Owner { + logger.Error("fetch-failed-owner-mismatch", models.ErrLockCollision, lager.Data{"fetched-owner": fetchedLock.Owner}) + return models.ErrLockCollision + } + + if fetchedLock.ModifiedId != lock.ModifiedId { + logger.Error("release-failed-id-mismatch", models.ErrLockCollision, lager.Data{"lock-modified-id": lock.ModifiedId, "fetched-modified-id": fetchedLock.ModifiedId}) + return models.ErrLockCollision + } + + if fetchedLock.ModifiedIndex != lock.ModifiedIndex { + logger.Error("release-failed-index-mismatch", models.ErrLockCollision, lager.Data{"lock-modified-index": lock.ModifiedIndex, "fetched-modified-index": fetchedLock.ModifiedIndex}) + return models.ErrLockCollision + } + + _, err = db.helper.Delete(ctx, logger, tx, "locks", + "path = ?", fetchedLock.Resource.Key, + ) + + if err != nil { + logger.Error("failed-to-release-lock", err) + return err + } + + logger.Info("released-lock") + + return nil + }) + + if err != nil { + if err == models.ErrResourceNotFound { + return false, nil + } + return false, err + } + + return true, nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/locket_health_check.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/locket_health_check.go new file mode 100644 index 0000000000..6b4c76d72e --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/locket_health_check.go @@ -0,0 +1,46 @@ +package db + +import ( + "context" + "fmt" + "time" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +//go:generate counterfeiter . LocketHealthCheckDB + +type LocketHealthCheckDB interface { + PerformLocketHealthCheck(ctx context.Context, logger lager.Logger, t time.Time) error +} + +func (db *SQLDB) PerformLocketHealthCheck(ctx context.Context, logger lager.Logger, t time.Time) error { + logger = logger.Session("db") + logger.Debug("starting") + defer logger.Debug("done") + + logger.Debug("upserting-time", lager.Data{"time": t}) + _, err := db.helper.Upsert( + ctx, + logger, + db, + "locket_health_check", + helpers.SQLAttributes{"id": 1, "time": t.UnixNano()}, + "id = ?", + 1, + ) + if err != nil { + return fmt.Errorf("failed upserting health check time: %s", err) + } + + logger.Debug("retrieving-upserted-time") + scanner := db.QueryRowContext(ctx, helpers.RebindForFlavor("SELECT time from locket_health_check where id = ?", db.flavor), 1) + var insertedTime int64 + err = scanner.Scan(&insertedTime) + if err != nil { + return fmt.Errorf("failed querying for health check time: %s", err) + } + logger.Debug("upserted-and-retrieved-time") + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/package.go new file mode 100644 index 0000000000..630e4755ec --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/package.go @@ -0,0 +1 @@ +package db // import "code.cloudfoundry.org/locket/db" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/queries.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/queries.go new file mode 100644 index 0000000000..f453e3ccb7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/queries.go @@ -0,0 +1,53 @@ +package db + +import ( + "context" + "fmt" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" +) + +func (db *SQLDB) CreateLockTable(ctx context.Context, logger lager.Logger) error { + _, err := db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS locks ( + path VARCHAR(255) PRIMARY KEY, + owner VARCHAR(255), + value VARCHAR(4096), + type VARCHAR(255) DEFAULT '', + modified_index BIGINT DEFAULT 0, + modified_id varchar(255) DEFAULT '', + ttl BIGINT DEFAULT 0 + ); + `) + if err != nil { + return err + } + + return nil +} + +func (db *SQLDB) CreateHealthCheckTable(ctx context.Context, logger lager.Logger) error { + logger = logger.Session("create-health-check-table") + logger.Info("starting") + defer logger.Info("completed") + + var createTableSQL string + switch db.flavor { + case helpers.MySQL: + createTableSQL = "CREATE TABLE IF NOT EXISTS locket_health_check (id int NOT NULL AUTO_INCREMENT, PRIMARY KEY (id), time bigint NOT NULL)" + case helpers.Postgres: + createTableSQL = "CREATE TABLE IF NOT EXISTS locket_health_check (id SERIAL PRIMARY KEY, time bigint NOT NULL)" + default: + return fmt.Errorf("unsupported database flavor: %s", db.flavor) + } + + logger.Info("creating-table") + _, err := db.ExecContext(ctx, helpers.RebindForFlavor(createTableSQL, db.flavor)) + if err != nil { + logger.Error("failed-creating-table", err) + return fmt.Errorf("failed to create health check table: %w", err) + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/sqldb.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/sqldb.go new file mode 100644 index 0000000000..7aa3e005d0 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/db/sqldb.go @@ -0,0 +1,48 @@ +package db + +import ( + "context" + + "code.cloudfoundry.org/diego-db-helpers/guidprovider" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/models" +) + +//go:generate counterfeiter . LockDB +type LockDB interface { + Lock(ctx context.Context, logger lager.Logger, resource *models.Resource, ttl int64) (*Lock, error) + Release(ctx context.Context, logger lager.Logger, resource *models.Resource) error + Fetch(ctx context.Context, logger lager.Logger, key string) (*Lock, error) + FetchAndRelease(ctx context.Context, logger lager.Logger, lock *Lock) (bool, error) + FetchAll(ctx context.Context, logger lager.Logger, lockType string) ([]*Lock, error) + Count(ctx context.Context, logger lager.Logger, lockType string) (int, error) +} + +type Lock struct { + *models.Resource + TtlInSeconds int64 + ModifiedIndex int64 + ModifiedId string +} + +type SQLDB struct { + helpers.QueryableDB + flavor string + helper helpers.SQLHelper + guidProvider guidprovider.GUIDProvider +} + +func NewSQLDB( + db helpers.QueryableDB, + flavor string, + guidProvider guidprovider.GUIDProvider, +) *SQLDB { + helper := helpers.NewSQLHelper(flavor) + return &SQLDB{ + QueryableDB: db, + flavor: flavor, + helper: helper, + guidProvider: guidProvider, + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/burglar.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/burglar.go new file mode 100644 index 0000000000..fa358b62b8 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/burglar.go @@ -0,0 +1,87 @@ +package expiration + +import ( + "context" + "os" + "time" + + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket" + "code.cloudfoundry.org/locket/db" +) + +const ( + locksExpiredCounter = "LocksExpired" + presenceExpiredCounter = "PresenceExpired" +) + +type burglar struct { + logger lager.Logger + lockDB db.LockDB + lockPick LockPick + clock clock.Clock + checkInterval time.Duration + metronClient loggingclient.IngressClient +} + +func NewBurglar(logger lager.Logger, lockDB db.LockDB, lockPick LockPick, clock clock.Clock, checkInterval time.Duration, metronClient loggingclient.IngressClient) burglar { + return burglar{ + logger: logger, + lockDB: lockDB, + lockPick: lockPick, + clock: clock, + checkInterval: checkInterval, + metronClient: metronClient, + } +} + +func (b burglar) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := b.logger.Session("burglar") + + logger.Info("started") + defer logger.Info("complete") + + locks, err := b.lockDB.FetchAll(context.Background(), logger, "") + if err != nil { + logger.Error("failed-fetching-locks", err) + } + + for _, lock := range locks { + b.lockPick.RegisterTTL(logger, lock) + } + + check := b.clock.NewTicker(b.checkInterval) + expirationCheck := b.clock.NewTicker(locket.ExpirationMetricsInterval) + + close(ready) + + for { + select { + case sig := <-signals: + logger.Info("signalled", lager.Data{"signal": sig}) + return nil + case <-check.C(): + locks, err := b.lockDB.FetchAll(context.Background(), logger, "") + if err != nil { + logger.Error("failed-fetching-locks", err) + continue + } + + for _, lock := range locks { + b.lockPick.RegisterTTL(logger, lock) + } + case <-expirationCheck.C(): + locksExpired, presencesExpired := b.lockPick.ExpirationCounts() + err := b.metronClient.SendMetric(locksExpiredCounter, int(locksExpired)) + if err != nil { + logger.Debug("failed-to-send-locks-expired-metric", lager.Data{"error": err}) + } + err = b.metronClient.SendMetric(presenceExpiredCounter, int(presencesExpired)) + if err != nil { + logger.Debug("failed-to-send-presences-expired-metric", lager.Data{"error": err}) + } + } + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/lock_pick.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/lock_pick.go new file mode 100644 index 0000000000..62c5844fd9 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/lock_pick.go @@ -0,0 +1,124 @@ +package expiration + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/db" + "code.cloudfoundry.org/locket/models" +) + +//go:generate counterfeiter . LockPick +type LockPick interface { + RegisterTTL(logger lager.Logger, lock *db.Lock) + ExpirationCounts() (uint32, uint32) // return lock and presence expirations, resp. +} + +type lockPick struct { + lockDB db.LockDB + clock clock.Clock + metronClient loggingclient.IngressClient + lockTTLs map[checkKey]chanAndIndex + lockMutex *sync.Mutex + presencesExpiredCount *uint32 + locksExpiredCount *uint32 +} + +type chanAndIndex struct { + channel chan struct{} + index int64 +} + +type checkKey struct { + key string + id string +} + +func NewLockPick(lockDB db.LockDB, clock clock.Clock, metronClient loggingclient.IngressClient) lockPick { + return lockPick{ + lockDB: lockDB, + clock: clock, + metronClient: metronClient, + lockTTLs: make(map[checkKey]chanAndIndex), + lockMutex: &sync.Mutex{}, + presencesExpiredCount: new(uint32), + locksExpiredCount: new(uint32), + } +} + +func (l lockPick) ExpirationCounts() (uint32, uint32) { + return atomic.LoadUint32(l.locksExpiredCount), atomic.LoadUint32(l.presencesExpiredCount) +} + +func (l lockPick) RegisterTTL(logger lager.Logger, lock *db.Lock) { + logger = logger.Session("register-ttl", lager.Data{"key": lock.Key, "modified-index": lock.ModifiedIndex, "type": lock.Type}) + logger.Debug("starting") + logger.Debug("completed") + + newChanIndex := chanAndIndex{ + channel: make(chan struct{}), + index: lock.ModifiedIndex, + } + l.lockMutex.Lock() + defer l.lockMutex.Unlock() + + channelIndex, ok := l.lockTTLs[checkKeyFromLock(lock)] + if ok && channelIndex.index >= newChanIndex.index { + logger.Debug("found-expiration-goroutine-for-index", lager.Data{"index": channelIndex.index}) + return + } + + if ok && channelIndex.index < newChanIndex.index { + close(channelIndex.channel) + } + + l.lockTTLs[checkKeyFromLock(lock)] = newChanIndex + go l.checkExpiration(logger, lock, newChanIndex.channel) +} + +func (l lockPick) checkExpiration(logger lager.Logger, lock *db.Lock, closeChan chan struct{}) { + lockTimer := l.clock.NewTimer(time.Duration(lock.TtlInSeconds) * time.Second) + + select { + case <-closeChan: + logger.Debug("cancelling-old-check-goroutine") + return + case <-lockTimer.C(): + defer func() { + l.lockMutex.Lock() + chanIndex := l.lockTTLs[checkKeyFromLock(lock)] + if chanIndex.index == lock.ModifiedIndex { + delete(l.lockTTLs, checkKeyFromLock(lock)) + } + l.lockMutex.Unlock() + }() + + expired, err := l.lockDB.FetchAndRelease(context.Background(), logger, lock) + if err != nil { + logger.Error("failed-compare-and-release", err) + return + } + + if expired { + logger.Info("lock-expired") + counter := l.locksExpiredCount + if lock.Type == models.PresenceType { + counter = l.presencesExpiredCount + } + atomic.AddUint32(counter, 1) + } + return + } +} + +func checkKeyFromLock(lock *db.Lock) checkKey { + return checkKey{ + key: lock.Key, + id: lock.ModifiedId, + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/package.go new file mode 100644 index 0000000000..55a456cd37 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/expiration/package.go @@ -0,0 +1 @@ +package expiration // import "code.cloudfoundry.org/locket/expiration" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/package.go new file mode 100644 index 0000000000..3f50972840 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/package.go @@ -0,0 +1 @@ +package grpcserver // import "code.cloudfoundry.org/locket/grpcserver" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/server.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/server.go new file mode 100644 index 0000000000..0fef3046b9 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/grpcserver/server.go @@ -0,0 +1,63 @@ +package grpcserver + +import ( + "crypto/tls" + "net" + "os" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/models" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type grpcServerRunner struct { + listenAddress string + handler models.LocketServer + logger lager.Logger + tlsConfig *tls.Config +} + +func NewGRPCServer(logger lager.Logger, listenAddress string, tlsConfig *tls.Config, handler models.LocketServer) grpcServerRunner { + return grpcServerRunner{ + listenAddress: listenAddress, + handler: handler, + logger: logger, + tlsConfig: tlsConfig, + } +} + +func (s grpcServerRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := s.logger.Session("grpc-server") + + logger.Info("started") + defer logger.Info("complete") + + lis, err := net.Listen("tcp", s.listenAddress) + if err != nil { + logger.Error("failed-to-listen", err) + return err + } + + server := grpc.NewServer(grpc.Creds(credentials.NewTLS(s.tlsConfig))) + models.RegisterLocketServer(server, s.handler) + + errCh := make(chan error) + go func() { + errCh <- server.Serve(lis) + }() + + close(ready) + + select { + case sig := <-signals: + logger.Info("signalled", lager.Data{"signal": sig}) + break + case err = <-errCh: + logger.Error("failed-to-serve", err) + break + } + + server.GracefulStop() + return err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/handler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/handler.go new file mode 100644 index 0000000000..b2bcb30525 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/handler.go @@ -0,0 +1,290 @@ +package handlers + +import ( + "time" + + "context" + + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/db" + "code.cloudfoundry.org/locket/expiration" + metrics_helpers "code.cloudfoundry.org/locket/metrics/helpers" + "code.cloudfoundry.org/locket/models" + "google.golang.org/grpc/metadata" +) + +const DefaultDBOperationTimeout = 10 * time.Second + +type locketHandler struct { + logger lager.Logger + + db db.LockDB + exitCh chan<- struct{} + lockPick expiration.LockPick + metrics metrics_helpers.RequestMetrics + dbOperationTimeout time.Duration +} + +func NewLocketHandler(logger lager.Logger, db db.LockDB, lockPick expiration.LockPick, requestMetrics metrics_helpers.RequestMetrics, exitCh chan<- struct{}, dbOperationTimeout time.Duration) *locketHandler { + return &locketHandler{ + logger: logger, + db: db, + lockPick: lockPick, + exitCh: exitCh, + metrics: requestMetrics, + dbOperationTimeout: dbOperationTimeout, + } +} + +func (h *locketHandler) newDBContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), h.dbOperationTimeout) +} + +func (h *locketHandler) exitIfUnrecoverable(err error) { + if err != helpers.ErrUnrecoverableError { + return + } + + h.logger.Error("unrecoverable-error", err) + + select { + case h.exitCh <- struct{}{}: + default: + } +} + +func (h *locketHandler) monitorRequest(requestType string, ctx context.Context, key string, owner string, f func() error) error { + h.metrics.IncrementRequestsStartedCounter(requestType, 1) + h.metrics.IncrementRequestsInFlightCounter(requestType, 1) + defer h.metrics.DecrementRequestsInFlightCounter(requestType, 1) + + start := time.Now() + + err := f() + + requestID := "" + if md, ok := metadata.FromIncomingContext(ctx); ok { + if vals := md.Get("uuid"); len(vals) > 0 { + requestID = vals[0] + } + } + + logData := lager.Data{ + "request-id": requestID, + "request-type": requestType, + "resource-key": key, + "resource-owner": owner, + } + if ctx.Err() == context.Canceled { + h.logger.Info("context-cancelled", logData) + h.metrics.IncrementRequestsCancelledCounter(requestType, 1) + } else if ctx.Err() == context.DeadlineExceeded { + h.logger.Info("context-deadline-exceeded", logData) + } + + h.metrics.UpdateLatency(requestType, time.Since(start)) + + if err != nil && err != models.ErrLockCollision { + h.metrics.IncrementRequestsFailedCounter(requestType, 1) + h.exitIfUnrecoverable(err) + } else { + h.metrics.IncrementRequestsSucceededCounter(requestType, 1) + } + return err +} + +func (h *locketHandler) Lock(ctx context.Context, req *models.LockRequest) (*models.LockResponse, error) { + var ( + response *models.LockResponse + err error + ) + + err = h.monitorRequest("Lock", ctx, req.Resource.Key, req.Resource.Owner, func() error { + response, err = h.lock(ctx, req) + return err + }) + + return response, err +} + +func (h *locketHandler) Release(ctx context.Context, req *models.ReleaseRequest) (*models.ReleaseResponse, error) { + var ( + response *models.ReleaseResponse + err error + ) + + err = h.monitorRequest("Release", ctx, req.Resource.Key, req.Resource.Owner, func() error { + response, err = h.release(req) + return err + }) + + return response, err +} + +func (h *locketHandler) Fetch(ctx context.Context, req *models.FetchRequest) (*models.FetchResponse, error) { + var ( + response *models.FetchResponse + err error + ) + + err = h.monitorRequest("Fetch", ctx, req.Key, "", func() error { + response, err = h.fetch(req) + return err + }) + + return response, err +} + +func (h *locketHandler) FetchAll(ctx context.Context, req *models.FetchAllRequest) (*models.FetchAllResponse, error) { + var ( + response *models.FetchAllResponse + err error + ) + + err = h.monitorRequest("FetchAll", ctx, "", "", func() error { + response, err = h.fetchAll(req) + return err + }) + + return response, err +} + +func (h *locketHandler) lock(ctx context.Context, req *models.LockRequest) (*models.LockResponse, error) { + logger := h.logger.Session("lock") + logger.Debug("started") + defer logger.Debug("complete") + + err := validate(req) + if err != nil { + logger.Error("invalid-request", err, lager.Data{"typeCode": req.Resource.GetTypeCode()}) + + return nil, err + } + + if req.TtlInSeconds <= 0 { + logger.Error("failed-locking-lock", models.ErrInvalidTTL, lager.Data{ + "key": req.Resource.Key, + "owner": req.Resource.Owner, + }) + return nil, models.ErrInvalidTTL + } + + if req.Resource.Owner == "" { + logger.Error("failed-locking-lock", models.ErrInvalidOwner, lager.Data{ + "key": req.Resource.Key, + "owner": req.Resource.Owner, + }) + return nil, models.ErrInvalidOwner + } + + md, _ := metadata.FromIncomingContext(ctx) + requestUUID := md["uuid"] + if len(requestUUID) > 0 { + logger = logger.WithData(lager.Data{"request-uuid": requestUUID[0]}) + } + + dbCtx, dbCancel := h.newDBContext() + defer dbCancel() + + lock, err := h.db.Lock(dbCtx, logger, req.Resource, req.TtlInSeconds) + if err != nil { + if err != models.ErrLockCollision { + logger.Error("failed-locking-lock", err, lager.Data{ + "key": req.Resource.Key, + "owner": req.Resource.Owner, + }) + } + return nil, err + } + + h.lockPick.RegisterTTL(logger, lock) + + return &models.LockResponse{}, nil +} + +func (h *locketHandler) release(req *models.ReleaseRequest) (*models.ReleaseResponse, error) { + logger := h.logger.Session("release") + logger.Debug("started") + defer logger.Debug("complete") + + dbCtx, dbCancel := h.newDBContext() + defer dbCancel() + + err := h.db.Release(dbCtx, logger, req.Resource) + if err != nil { + return nil, err + } + + return &models.ReleaseResponse{}, nil +} + +func (h *locketHandler) fetch(req *models.FetchRequest) (*models.FetchResponse, error) { + logger := h.logger.Session("fetch") + logger.Debug("started") + defer logger.Debug("complete") + + dbCtx, dbCancel := h.newDBContext() + defer dbCancel() + + lock, err := h.db.Fetch(dbCtx, logger, req.Key) + if err != nil { + return nil, err + } + + return &models.FetchResponse{ + Resource: lock.Resource, + }, nil +} + +func (h *locketHandler) fetchAll(req *models.FetchAllRequest) (*models.FetchAllResponse, error) { + logger := h.logger.Session("fetch-all") + logger.Debug("started") + defer logger.Debug("complete") + + err := validate(req) + if err != nil { + logger.Error("invalid-request", err, lager.Data{"typeCode": req.GetTypeCode()}) + return nil, err + } + + dbCtx, dbCancel := h.newDBContext() + defer dbCancel() + + locks, err := h.db.FetchAll(dbCtx, logger, models.GetType(&models.Resource{TypeCode: req.TypeCode})) + if err != nil { + return nil, err + } + + var responses []*models.Resource + for _, lock := range locks { + responses = append(responses, lock.Resource) + } + + return &models.FetchAllResponse{ + Resources: responses, + }, nil +} + +func validate(req interface{}) error { + var reqTypeCode models.TypeCode + + switch incomingReq := req.(type) { + case *models.LockRequest: + reqTypeCode = incomingReq.Resource.GetTypeCode() + case *models.FetchAllRequest: + reqTypeCode = incomingReq.GetTypeCode() + default: + return nil + } + + if _, found := models.TypeCode_name[int32(reqTypeCode)]; !found { + return models.ErrInvalidType + } + + if reqTypeCode == models.UNKNOWN { + return models.ErrInvalidType + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/package.go new file mode 100644 index 0000000000..aeb659cf93 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/handlers/package.go @@ -0,0 +1 @@ +package handlers // import "code.cloudfoundry.org/locket/handlers" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/db_metrics.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/db_metrics.go new file mode 100644 index 0000000000..42dd9d861f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/db_metrics.go @@ -0,0 +1,113 @@ +package metrics + +import ( + "os" + "time" + + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers" + "code.cloudfoundry.org/diego-db-helpers/sqldb/helpers/monitor" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "github.com/tedsuo/ifrit" +) + +const ( + dbOpenConnectionsMetric = "DBOpenConnections" + dbWaitDurationMetric = "DBWaitDuration" + dbWaitCountMetric = "DBWaitCount" + dbQueriesTotalMetric = "DBQueriesTotal" + dbQueriesSucceededMetric = "DBQueriesSucceeded" + dbQueriesFailedMetric = "DBQueriesFailed" + dbQueriesInFlightMetric = "DBQueriesInFlight" + dbQueryDurationMaxMetric = "DBQueryDurationMax" +) + +type dbMetricsNotifier struct { + logger lager.Logger + ticker clock.Clock + metricsInterval time.Duration + lockDB helpers.QueryableDB + metronClient loggingclient.IngressClient + queryMonitor monitor.Monitor +} + +func NewDBMetricsNotifier(logger lager.Logger, ticker clock.Clock, metronClient loggingclient.IngressClient, metricsInterval time.Duration, lockDB helpers.QueryableDB, queryMonitor monitor.Monitor) ifrit.Runner { + return &dbMetricsNotifier{ + logger: logger, + ticker: ticker, + metricsInterval: metricsInterval, + lockDB: lockDB, + metronClient: metronClient, + queryMonitor: queryMonitor, + } +} + +func (notifier *dbMetricsNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.logger.Session("metrics-notifier") + logger.Info("starting", lager.Data{"interval": notifier.metricsInterval}) + defer logger.Info("completed") + close(ready) + + tick := notifier.ticker.NewTicker(notifier.metricsInterval) + for { + select { + case <-signals: + return nil + case <-tick.C(): + logger.Debug("emitting-metrics") + + openConnections := notifier.lockDB.OpenConnections() + waitDuration := notifier.lockDB.WaitDuration() + waitCount := notifier.lockDB.WaitCount() + queriesTotal := notifier.queryMonitor.Total() + queriesSucceeded := notifier.queryMonitor.Succeeded() + queriesFailed := notifier.queryMonitor.Failed() + queriesInFlightMax := notifier.queryMonitor.ReadAndResetInFlightMax() + queryDurationMax := notifier.queryMonitor.ReadAndResetDurationMax() + + err := notifier.metronClient.SendMetric(dbOpenConnectionsMetric, openConnections) + if err != nil { + logger.Error("failed-sending-db-open-connections-count", err) + } + + err = notifier.metronClient.SendDuration(dbWaitDurationMetric, waitDuration) + if err != nil { + logger.Error("failed-sending-db-wait-duration", err) + } + + err = notifier.metronClient.SendMetric(dbWaitCountMetric, int(waitCount)) + if err != nil { + logger.Error("failed-sending-db-wait-count", err) + } + + logger.Debug("sending-queries-total-metric", lager.Data{"value": queriesTotal}) + err = notifier.metronClient.SendMetric(dbQueriesTotalMetric, int(queriesTotal)) + if err != nil { + logger.Error("failed-sending-db-queries-total-count", err) + } + + err = notifier.metronClient.SendMetric(dbQueriesSucceededMetric, int(queriesSucceeded)) + if err != nil { + logger.Error("failed-sending-db-queries-succeeded-count", err) + } + + err = notifier.metronClient.SendMetric(dbQueriesFailedMetric, int(queriesFailed)) + if err != nil { + logger.Error("failed-sending-db-queries-failed-count", err) + } + + err = notifier.metronClient.SendMetric(dbQueriesInFlightMetric, int(queriesInFlightMax)) + if err != nil { + logger.Error("failed-sending-db-queries-in-flight-count", err) + } + + err = notifier.metronClient.SendDuration(dbQueryDurationMaxMetric, queryDurationMax) + if err != nil { + logger.Error("failed-sending-db-query-duration-max", err) + } + + logger.Debug("emitted-metrics") + } + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/lock_metrics.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/lock_metrics.go new file mode 100644 index 0000000000..5d9764ec2e --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/lock_metrics.go @@ -0,0 +1,76 @@ +package metrics + +import ( + "context" + "os" + "time" + + "code.cloudfoundry.org/clock" + loggingclient "code.cloudfoundry.org/diego-logging-client" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/locket/db" + "code.cloudfoundry.org/locket/models" + "github.com/tedsuo/ifrit" +) + +const ( + activeLocksMetric = "ActiveLocks" + activePresencesMetric = "ActivePresences" +) + +type lockMetricsNotifier struct { + logger lager.Logger + ticker clock.Clock + metricsInterval time.Duration + lockDB db.LockDB + metronClient loggingclient.IngressClient +} + +func NewLockMetricsNotifier(logger lager.Logger, ticker clock.Clock, metronClient loggingclient.IngressClient, metricsInterval time.Duration, lockDB db.LockDB) ifrit.Runner { + return &lockMetricsNotifier{ + logger: logger, + ticker: ticker, + metricsInterval: metricsInterval, + lockDB: lockDB, + metronClient: metronClient, + } +} + +func (notifier *lockMetricsNotifier) Run(signals <-chan os.Signal, ready chan<- struct{}) error { + logger := notifier.logger.Session("lock-metrics-notifier") + logger.Info("starting", lager.Data{"interval": notifier.metricsInterval}) + defer logger.Info("completed") + close(ready) + + tick := notifier.ticker.NewTicker(notifier.metricsInterval) + for { + select { + case <-signals: + return nil + case <-tick.C(): + logger.Debug("emitting-metrics") + + locks, err := notifier.lockDB.Count(context.Background(), logger, models.LockType) + if err != nil { + logger.Error("failed-to-retrieve-lock-count", err) + } else { + err = notifier.metronClient.SendMetric(activeLocksMetric, locks) + if err != nil { + logger.Error("failed-sending-lock-count", err) + } + } + + presences, err := notifier.lockDB.Count(context.Background(), logger, models.PresenceType) + if err != nil { + logger.Error("failed-to-retrieve-presence-count", err) + } else { + err = notifier.metronClient.SendMetric(activePresencesMetric, presences) + if err != nil { + logger.Error("failed-sending-presences-count", err) + } + } + + logger.Debug("emitted-metrics") + } + } +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/package.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/package.go new file mode 100644 index 0000000000..d595914ccf --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/locket/metrics/package.go @@ -0,0 +1 @@ +package metrics // import "code.cloudfoundry.org/locket/metrics" diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/LICENSE b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/LICENSE new file mode 100644 index 0000000000..d0381d6d04 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/LICENSE @@ -0,0 +1,176 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/NOTICE b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/NOTICE new file mode 100644 index 0000000000..2f4e1e8f40 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/NOTICE @@ -0,0 +1,13 @@ +Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/cfroutes/routing_info_helpers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/cfroutes/routing_info_helpers.go new file mode 100644 index 0000000000..a7536a3aea --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/cfroutes/routing_info_helpers.go @@ -0,0 +1,49 @@ +package cfroutes + +import ( + "encoding/json" + + "code.cloudfoundry.org/bbs/models" +) + +const CF_ROUTER = "cf-router" + +type CFRoutes []CFRoute + +type CFRoute struct { + Hostnames []string `json:"hostnames"` + Port uint32 `json:"port"` + RouteServiceUrl string `json:"route_service_url,omitempty"` + IsolationSegment string `json:"isolation_segment,omitempty"` + Protocol string `json:"protocol,omitempty"` + Options json.RawMessage `json:"options,omitempty"` +} + +func (c CFRoutes) RoutingInfo() models.Routes { + data, _ := json.Marshal(c) + routingInfo := json.RawMessage(data) + return models.Routes{ + CF_ROUTER: &routingInfo, + } +} + +func CFRoutesFromRoutingInfo(routingInfo models.Routes) (CFRoutes, error) { + if routingInfo == nil { + return nil, nil + } + + routes := routingInfo + data, found := routes[CF_ROUTER] + if !found { + return nil, nil + } + + if data == nil { + return nil, nil + } + + cfRoutes := CFRoutes{} + err := json.Unmarshal(*data, &cfRoutes) + + return cfRoutes, err +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/internalroutes/routing_info_helpers.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/internalroutes/routing_info_helpers.go new file mode 100644 index 0000000000..78f120b0ce --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/internalroutes/routing_info_helpers.go @@ -0,0 +1,18 @@ +package internalroutes + +import ( + "code.cloudfoundry.org/bbs/models" +) + +const INTERNAL_ROUTER = models.InternalRouter + +// InternalRoute and InternalRoutes are type aliases for the canonical types +// now defined in code.cloudfoundry.org/bbs/models to break the bbs→internalroutes +// import cycle while keeping all existing callers building unchanged. +type InternalRoute = models.InternalRoute +type InternalRoutes = models.InternalRoutes + +// InternalRoutesFromRoutingInfo delegates to the canonical implementation in bbs/models. +func InternalRoutesFromRoutingInfo(routingInfo models.Routes) (InternalRoutes, error) { + return models.InternalRoutesFromRoutingInfo(routingInfo) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/tcp_routes/routes_helper.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/tcp_routes/routes_helper.go new file mode 100644 index 0000000000..af37c4c2c2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/routing-info/tcp_routes/routes_helper.go @@ -0,0 +1,49 @@ +package tcp_routes + +import ( + "encoding/json" + + "code.cloudfoundry.org/bbs/models" +) + +const TCP_ROUTER = "tcp-router" + +type TCPRoutes []TCPRoute + +type TCPRoute struct { + RouterGroupGuid string `json:"router_group_guid"` + ExternalPort uint32 `json:"external_port"` + ContainerPort uint32 `json:"container_port"` + SniHostname *string `json:"backend_sni_hostname,omitempty"` + TerminateFrontendTLS bool `json:"terminate_frontend_tls,omitempty"` +} + +func (c TCPRoutes) RoutingInfo() *models.Routes { + data, _ := json.Marshal(c) + routingInfo := json.RawMessage(data) + return &models.Routes{ + TCP_ROUTER: &routingInfo, + } +} + +func TCPRoutesFromRoutingInfo(routingInfoPtr *models.Routes) (TCPRoutes, error) { + if routingInfoPtr == nil { + return nil, nil + } + + routingInfo := *routingInfoPtr + + data, found := routingInfo[TCP_ROUTER] + if !found { + return nil, nil + } + + if data == nil { + return nil, nil + } + + routes := TCPRoutes{} + err := json.Unmarshal(*data, &routes) + + return routes, err +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/LICENSE.txt b/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/LICENSE.txt deleted file mode 100644 index 24b53065f4..0000000000 --- a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2016 Caleb Spare - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/README.md b/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/README.md deleted file mode 100644 index 33c88305c4..0000000000 --- a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# xxhash - -[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2) -[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml) - -xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a -high-quality hashing algorithm that is much faster than anything in the Go -standard library. - -This package provides a straightforward API: - -``` -func Sum64(b []byte) uint64 -func Sum64String(s string) uint64 -type Digest struct{ ... } - func New() *Digest -``` - -The `Digest` type implements hash.Hash64. Its key methods are: - -``` -func (*Digest) Write([]byte) (int, error) -func (*Digest) WriteString(string) (int, error) -func (*Digest) Sum64() uint64 -``` - -The package is written with optimized pure Go and also contains even faster -assembly implementations for amd64 and arm64. If desired, the `purego` build tag -opts into using the Go code even on those architectures. - -[xxHash]: http://cyan4973.github.io/xxHash/ - -## Compatibility - -This package is in a module and the latest code is in version 2 of the module. -You need a version of Go with at least "minimal module compatibility" to use -github.com/cespare/xxhash/v2: - -* 1.9.7+ for Go 1.9 -* 1.10.3+ for Go 1.10 -* Go 1.11 or later - -I recommend using the latest release of Go. - -## Benchmarks - -Here are some quick benchmarks comparing the pure-Go and assembly -implementations of Sum64. - -| input size | purego | asm | -| ---------- | --------- | --------- | -| 4 B | 1.3 GB/s | 1.2 GB/s | -| 16 B | 2.9 GB/s | 3.5 GB/s | -| 100 B | 6.9 GB/s | 8.1 GB/s | -| 4 KB | 11.7 GB/s | 16.7 GB/s | -| 10 MB | 12.0 GB/s | 17.3 GB/s | - -These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C -CPU using the following commands under Go 1.19.2: - -``` -benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$') -benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$') -``` - -## Projects using this package - -- [InfluxDB](https://github.com/influxdata/influxdb) -- [Prometheus](https://github.com/prometheus/prometheus) -- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) -- [FreeCache](https://github.com/coocood/freecache) -- [FastCache](https://github.com/VictoriaMetrics/fastcache) -- [Ristretto](https://github.com/dgraph-io/ristretto) -- [Badger](https://github.com/dgraph-io/badger) diff --git a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/testall.sh b/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/testall.sh deleted file mode 100644 index 94b9c44398..0000000000 --- a/src/code.cloudfoundry.org/vendor/github.com/cespare/xxhash/v2/testall.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -# Small convenience script for running the tests with various combinations of -# arch/tags. This assumes we're running on amd64 and have qemu available. - -go test ./... -go test -tags purego ./... -GOARCH=arm64 go test -GOARCH=arm64 go test -tags purego diff --git a/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/LICENSE b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.pb.go b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.pb.go new file mode 100644 index 0000000000..8a5b8ff2b9 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.pb.go @@ -0,0 +1,1097 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v6.30.2 +// source: workload.proto + +package workload + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The X509SVIDRequest message conveys parameters for requesting an X.509-SVID. +// There are currently no request parameters. +type X509SVIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *X509SVIDRequest) Reset() { + *x = X509SVIDRequest{} + mi := &file_workload_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *X509SVIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509SVIDRequest) ProtoMessage() {} + +func (x *X509SVIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509SVIDRequest.ProtoReflect.Descriptor instead. +func (*X509SVIDRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{0} +} + +// The X509SVIDResponse message carries X.509-SVIDs and related information, +// including a set of global CRLs and a list of bundles the workload may use +// for federating with foreign trust domains. +type X509SVIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. A list of X509SVID messages, each of which includes a single + // X.509-SVID, its private key, and the bundle for the trust domain. + Svids []*X509SVID `protobuf:"bytes,1,rep,name=svids,proto3" json:"svids,omitempty"` + // Optional. ASN.1 DER encoded certificate revocation lists. + Crl [][]byte `protobuf:"bytes,2,rep,name=crl,proto3" json:"crl,omitempty"` + // Optional. CA certificate bundles belonging to foreign trust domains that + // the workload should trust, keyed by the SPIFFE ID of the foreign trust + // domain. Bundles are ASN.1 DER encoded. + FederatedBundles map[string][]byte `protobuf:"bytes,3,rep,name=federated_bundles,json=federatedBundles,proto3" json:"federated_bundles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *X509SVIDResponse) Reset() { + *x = X509SVIDResponse{} + mi := &file_workload_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *X509SVIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509SVIDResponse) ProtoMessage() {} + +func (x *X509SVIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509SVIDResponse.ProtoReflect.Descriptor instead. +func (*X509SVIDResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{1} +} + +func (x *X509SVIDResponse) GetSvids() []*X509SVID { + if x != nil { + return x.Svids + } + return nil +} + +func (x *X509SVIDResponse) GetCrl() [][]byte { + if x != nil { + return x.Crl + } + return nil +} + +func (x *X509SVIDResponse) GetFederatedBundles() map[string][]byte { + if x != nil { + return x.FederatedBundles + } + return nil +} + +// The X509SVID message carries a single SVID and all associated information, +// including the X.509 bundle for the trust domain. +type X509SVID struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The SPIFFE ID of the SVID in this entry + SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + // Required. ASN.1 DER encoded certificate chain. MAY include + // intermediates, the leaf certificate (or SVID itself) MUST come first. + X509Svid []byte `protobuf:"bytes,2,opt,name=x509_svid,json=x509Svid,proto3" json:"x509_svid,omitempty"` + // Required. ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted. + X509SvidKey []byte `protobuf:"bytes,3,opt,name=x509_svid_key,json=x509SvidKey,proto3" json:"x509_svid_key,omitempty"` + // Required. ASN.1 DER encoded X.509 bundle for the trust domain. + Bundle []byte `protobuf:"bytes,4,opt,name=bundle,proto3" json:"bundle,omitempty"` + // Optional. An operator-specified string used to provide guidance on how this + // identity should be used by a workload when more than one SVID is returned. + // For example, `internal` and `external` to indicate an SVID for internal or + // external use, respectively. + Hint string `protobuf:"bytes,5,opt,name=hint,proto3" json:"hint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *X509SVID) Reset() { + *x = X509SVID{} + mi := &file_workload_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *X509SVID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509SVID) ProtoMessage() {} + +func (x *X509SVID) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509SVID.ProtoReflect.Descriptor instead. +func (*X509SVID) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{2} +} + +func (x *X509SVID) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +func (x *X509SVID) GetX509Svid() []byte { + if x != nil { + return x.X509Svid + } + return nil +} + +func (x *X509SVID) GetX509SvidKey() []byte { + if x != nil { + return x.X509SvidKey + } + return nil +} + +func (x *X509SVID) GetBundle() []byte { + if x != nil { + return x.Bundle + } + return nil +} + +func (x *X509SVID) GetHint() string { + if x != nil { + return x.Hint + } + return "" +} + +// The X509BundlesRequest message conveys parameters for requesting X.509 +// bundles. There are currently no such parameters. +type X509BundlesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *X509BundlesRequest) Reset() { + *x = X509BundlesRequest{} + mi := &file_workload_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *X509BundlesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509BundlesRequest) ProtoMessage() {} + +func (x *X509BundlesRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509BundlesRequest.ProtoReflect.Descriptor instead. +func (*X509BundlesRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{3} +} + +// The X509BundlesResponse message carries a set of global CRLs and a map of +// trust bundles the workload should trust. +type X509BundlesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. ASN.1 DER encoded certificate revocation lists. + Crl [][]byte `protobuf:"bytes,1,rep,name=crl,proto3" json:"crl,omitempty"` + // Required. CA certificate bundles belonging to trust domains that the + // workload should trust, keyed by the SPIFFE ID of the trust domain. + // Bundles are ASN.1 DER encoded. + Bundles map[string][]byte `protobuf:"bytes,2,rep,name=bundles,proto3" json:"bundles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *X509BundlesResponse) Reset() { + *x = X509BundlesResponse{} + mi := &file_workload_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *X509BundlesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509BundlesResponse) ProtoMessage() {} + +func (x *X509BundlesResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509BundlesResponse.ProtoReflect.Descriptor instead. +func (*X509BundlesResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{4} +} + +func (x *X509BundlesResponse) GetCrl() [][]byte { + if x != nil { + return x.Crl + } + return nil +} + +func (x *X509BundlesResponse) GetBundles() map[string][]byte { + if x != nil { + return x.Bundles + } + return nil +} + +type JWTSVIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The audience(s) the workload intends to authenticate against. + Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` + // Optional. The requested SPIFFE ID for the JWT-SVID. If unset, all + // JWT-SVIDs to which the workload is entitled are requested. + SpiffeId string `protobuf:"bytes,2,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JWTSVIDRequest) Reset() { + *x = JWTSVIDRequest{} + mi := &file_workload_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JWTSVIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JWTSVIDRequest) ProtoMessage() {} + +func (x *JWTSVIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JWTSVIDRequest.ProtoReflect.Descriptor instead. +func (*JWTSVIDRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{5} +} + +func (x *JWTSVIDRequest) GetAudience() []string { + if x != nil { + return x.Audience + } + return nil +} + +func (x *JWTSVIDRequest) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +// The JWTSVIDResponse message conveys JWT-SVIDs. +type JWTSVIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The list of returned JWT-SVIDs. + Svids []*JWTSVID `protobuf:"bytes,1,rep,name=svids,proto3" json:"svids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JWTSVIDResponse) Reset() { + *x = JWTSVIDResponse{} + mi := &file_workload_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JWTSVIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JWTSVIDResponse) ProtoMessage() {} + +func (x *JWTSVIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JWTSVIDResponse.ProtoReflect.Descriptor instead. +func (*JWTSVIDResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{6} +} + +func (x *JWTSVIDResponse) GetSvids() []*JWTSVID { + if x != nil { + return x.Svids + } + return nil +} + +// The JWTSVID message carries the JWT-SVID token and associated metadata. +type JWTSVID struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The SPIFFE ID of the JWT-SVID. + SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + // Required. Encoded JWT using JWS Compact Serialization. + Svid string `protobuf:"bytes,2,opt,name=svid,proto3" json:"svid,omitempty"` + // Optional. An operator-specified string used to provide guidance on how this + // identity should be used by a workload when more than one SVID is returned. + // For example, `internal` and `external` to indicate an SVID for internal or + // external use, respectively. + Hint string `protobuf:"bytes,3,opt,name=hint,proto3" json:"hint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JWTSVID) Reset() { + *x = JWTSVID{} + mi := &file_workload_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JWTSVID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JWTSVID) ProtoMessage() {} + +func (x *JWTSVID) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JWTSVID.ProtoReflect.Descriptor instead. +func (*JWTSVID) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{7} +} + +func (x *JWTSVID) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +func (x *JWTSVID) GetSvid() string { + if x != nil { + return x.Svid + } + return "" +} + +func (x *JWTSVID) GetHint() string { + if x != nil { + return x.Hint + } + return "" +} + +// The JWTBundlesRequest message conveys parameters for requesting JWT bundles. +// There are currently no such parameters. +type JWTBundlesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JWTBundlesRequest) Reset() { + *x = JWTBundlesRequest{} + mi := &file_workload_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JWTBundlesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JWTBundlesRequest) ProtoMessage() {} + +func (x *JWTBundlesRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JWTBundlesRequest.ProtoReflect.Descriptor instead. +func (*JWTBundlesRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{8} +} + +// The JWTBundlesReponse conveys JWT bundles. +type JWTBundlesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust + // domain. + Bundles map[string][]byte `protobuf:"bytes,1,rep,name=bundles,proto3" json:"bundles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JWTBundlesResponse) Reset() { + *x = JWTBundlesResponse{} + mi := &file_workload_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JWTBundlesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JWTBundlesResponse) ProtoMessage() {} + +func (x *JWTBundlesResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JWTBundlesResponse.ProtoReflect.Descriptor instead. +func (*JWTBundlesResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{9} +} + +func (x *JWTBundlesResponse) GetBundles() map[string][]byte { + if x != nil { + return x.Bundles + } + return nil +} + +// The ValidateJWTSVIDRequest message conveys request parameters for +// JWT-SVID validation. +type ValidateJWTSVIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The audience of the validating party. The JWT-SVID must + // contain an audience claim which contains this value in order to + // succesfully validate. + Audience string `protobuf:"bytes,1,opt,name=audience,proto3" json:"audience,omitempty"` + // Required. The JWT-SVID to validate, encoded using JWS Compact + // Serialization. + Svid string `protobuf:"bytes,2,opt,name=svid,proto3" json:"svid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateJWTSVIDRequest) Reset() { + *x = ValidateJWTSVIDRequest{} + mi := &file_workload_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateJWTSVIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateJWTSVIDRequest) ProtoMessage() {} + +func (x *ValidateJWTSVIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateJWTSVIDRequest.ProtoReflect.Descriptor instead. +func (*ValidateJWTSVIDRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{10} +} + +func (x *ValidateJWTSVIDRequest) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *ValidateJWTSVIDRequest) GetSvid() string { + if x != nil { + return x.Svid + } + return "" +} + +// The ValidateJWTSVIDReponse message conveys the JWT-SVID validation results. +type ValidateJWTSVIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The SPIFFE ID of the validated JWT-SVID. + SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + // Optional. Arbitrary claims contained within the payload of the validated + // JWT-SVID. + Claims *structpb.Struct `protobuf:"bytes,2,opt,name=claims,proto3" json:"claims,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateJWTSVIDResponse) Reset() { + *x = ValidateJWTSVIDResponse{} + mi := &file_workload_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateJWTSVIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateJWTSVIDResponse) ProtoMessage() {} + +func (x *ValidateJWTSVIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateJWTSVIDResponse.ProtoReflect.Descriptor instead. +func (*ValidateJWTSVIDResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{11} +} + +func (x *ValidateJWTSVIDResponse) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +func (x *ValidateJWTSVIDResponse) GetClaims() *structpb.Struct { + if x != nil { + return x.Claims + } + return nil +} + +// WITSVIDRequest conveys parameters for requesting WIT-SVIDs. +type WITSVIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The requested SPIFFE ID for the WIT-SVID. If unset, all + // WIT-SVIDs to which the workload is entitled are requested. + SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WITSVIDRequest) Reset() { + *x = WITSVIDRequest{} + mi := &file_workload_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WITSVIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WITSVIDRequest) ProtoMessage() {} + +func (x *WITSVIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WITSVIDRequest.ProtoReflect.Descriptor instead. +func (*WITSVIDRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{12} +} + +func (x *WITSVIDRequest) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +// WITSVIDResponse conveys WIT-SVIDs. +type WITSVIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The list of returned WIT-SVIDs. + Svids []*WITSVID `protobuf:"bytes,1,rep,name=svids,proto3" json:"svids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WITSVIDResponse) Reset() { + *x = WITSVIDResponse{} + mi := &file_workload_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WITSVIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WITSVIDResponse) ProtoMessage() {} + +func (x *WITSVIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WITSVIDResponse.ProtoReflect.Descriptor instead. +func (*WITSVIDResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{13} +} + +func (x *WITSVIDResponse) GetSvids() []*WITSVID { + if x != nil { + return x.Svids + } + return nil +} + +// WITSVID carries a single WIT-SVID and associated metadata. +type WITSVID struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The SPIFFE ID of the WIT-SVID. + SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` + // Required. Encoded WIT-SVID using JWS Compact Serialization. + WitSvid string `protobuf:"bytes,2,opt,name=wit_svid,json=witSvid,proto3" json:"wit_svid,omitempty"` + // Required. JWK-encoded private key bound to this WIT-SVID. + WitSvidKey string `protobuf:"bytes,3,opt,name=wit_svid_key,json=witSvidKey,proto3" json:"wit_svid_key,omitempty"` + // Optional. An operator-specified string used to provide guidance on how + // this identity should be used by a workload when more than one SVID is + // returned. + Hint string `protobuf:"bytes,4,opt,name=hint,proto3" json:"hint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WITSVID) Reset() { + *x = WITSVID{} + mi := &file_workload_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WITSVID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WITSVID) ProtoMessage() {} + +func (x *WITSVID) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WITSVID.ProtoReflect.Descriptor instead. +func (*WITSVID) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{14} +} + +func (x *WITSVID) GetSpiffeId() string { + if x != nil { + return x.SpiffeId + } + return "" +} + +func (x *WITSVID) GetWitSvid() string { + if x != nil { + return x.WitSvid + } + return "" +} + +func (x *WITSVID) GetWitSvidKey() string { + if x != nil { + return x.WitSvidKey + } + return "" +} + +func (x *WITSVID) GetHint() string { + if x != nil { + return x.Hint + } + return "" +} + +// WITBundlesRequest conveys parameters for requesting WIT bundles. +// There are currently no such parameters. +type WITBundlesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WITBundlesRequest) Reset() { + *x = WITBundlesRequest{} + mi := &file_workload_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WITBundlesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WITBundlesRequest) ProtoMessage() {} + +func (x *WITBundlesRequest) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WITBundlesRequest.ProtoReflect.Descriptor instead. +func (*WITBundlesRequest) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{15} +} + +// WITBundlesResponse conveys WIT bundles. +type WITBundlesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. JWK encoded WIT bundles, keyed by the SPIFFE ID of the trust + // domain. + Bundles map[string]string `protobuf:"bytes,1,rep,name=bundles,proto3" json:"bundles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WITBundlesResponse) Reset() { + *x = WITBundlesResponse{} + mi := &file_workload_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WITBundlesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WITBundlesResponse) ProtoMessage() {} + +func (x *WITBundlesResponse) ProtoReflect() protoreflect.Message { + mi := &file_workload_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WITBundlesResponse.ProtoReflect.Descriptor instead. +func (*WITBundlesResponse) Descriptor() ([]byte, []int) { + return file_workload_proto_rawDescGZIP(), []int{16} +} + +func (x *WITBundlesResponse) GetBundles() map[string]string { + if x != nil { + return x.Bundles + } + return nil +} + +var File_workload_proto protoreflect.FileDescriptor + +const file_workload_proto_rawDesc = "" + + "\n" + + "\x0eworkload.proto\x1a\x1cgoogle/protobuf/struct.proto\"\x11\n" + + "\x0fX509SVIDRequest\"\xe0\x01\n" + + "\x10X509SVIDResponse\x12\x1f\n" + + "\x05svids\x18\x01 \x03(\v2\t.X509SVIDR\x05svids\x12\x10\n" + + "\x03crl\x18\x02 \x03(\fR\x03crl\x12T\n" + + "\x11federated_bundles\x18\x03 \x03(\v2'.X509SVIDResponse.FederatedBundlesEntryR\x10federatedBundles\x1aC\n" + + "\x15FederatedBundlesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\x94\x01\n" + + "\bX509SVID\x12\x1b\n" + + "\tspiffe_id\x18\x01 \x01(\tR\bspiffeId\x12\x1b\n" + + "\tx509_svid\x18\x02 \x01(\fR\bx509Svid\x12\"\n" + + "\rx509_svid_key\x18\x03 \x01(\fR\vx509SvidKey\x12\x16\n" + + "\x06bundle\x18\x04 \x01(\fR\x06bundle\x12\x12\n" + + "\x04hint\x18\x05 \x01(\tR\x04hint\"\x14\n" + + "\x12X509BundlesRequest\"\xa0\x01\n" + + "\x13X509BundlesResponse\x12\x10\n" + + "\x03crl\x18\x01 \x03(\fR\x03crl\x12;\n" + + "\abundles\x18\x02 \x03(\v2!.X509BundlesResponse.BundlesEntryR\abundles\x1a:\n" + + "\fBundlesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"I\n" + + "\x0eJWTSVIDRequest\x12\x1a\n" + + "\baudience\x18\x01 \x03(\tR\baudience\x12\x1b\n" + + "\tspiffe_id\x18\x02 \x01(\tR\bspiffeId\"1\n" + + "\x0fJWTSVIDResponse\x12\x1e\n" + + "\x05svids\x18\x01 \x03(\v2\b.JWTSVIDR\x05svids\"N\n" + + "\aJWTSVID\x12\x1b\n" + + "\tspiffe_id\x18\x01 \x01(\tR\bspiffeId\x12\x12\n" + + "\x04svid\x18\x02 \x01(\tR\x04svid\x12\x12\n" + + "\x04hint\x18\x03 \x01(\tR\x04hint\"\x13\n" + + "\x11JWTBundlesRequest\"\x8c\x01\n" + + "\x12JWTBundlesResponse\x12:\n" + + "\abundles\x18\x01 \x03(\v2 .JWTBundlesResponse.BundlesEntryR\abundles\x1a:\n" + + "\fBundlesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"H\n" + + "\x16ValidateJWTSVIDRequest\x12\x1a\n" + + "\baudience\x18\x01 \x01(\tR\baudience\x12\x12\n" + + "\x04svid\x18\x02 \x01(\tR\x04svid\"g\n" + + "\x17ValidateJWTSVIDResponse\x12\x1b\n" + + "\tspiffe_id\x18\x01 \x01(\tR\bspiffeId\x12/\n" + + "\x06claims\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x06claims\"-\n" + + "\x0eWITSVIDRequest\x12\x1b\n" + + "\tspiffe_id\x18\x01 \x01(\tR\bspiffeId\"1\n" + + "\x0fWITSVIDResponse\x12\x1e\n" + + "\x05svids\x18\x01 \x03(\v2\b.WITSVIDR\x05svids\"w\n" + + "\aWITSVID\x12\x1b\n" + + "\tspiffe_id\x18\x01 \x01(\tR\bspiffeId\x12\x19\n" + + "\bwit_svid\x18\x02 \x01(\tR\awitSvid\x12 \n" + + "\fwit_svid_key\x18\x03 \x01(\tR\n" + + "witSvidKey\x12\x12\n" + + "\x04hint\x18\x04 \x01(\tR\x04hint\"\x13\n" + + "\x11WITBundlesRequest\"\x8c\x01\n" + + "\x12WITBundlesResponse\x12:\n" + + "\abundles\x18\x01 \x03(\v2 .WITBundlesResponse.BundlesEntryR\abundles\x1a:\n" + + "\fBundlesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xb6\x03\n" + + "\x11SpiffeWorkloadAPI\x126\n" + + "\rFetchX509SVID\x12\x10.X509SVIDRequest\x1a\x11.X509SVIDResponse0\x01\x12?\n" + + "\x10FetchX509Bundles\x12\x13.X509BundlesRequest\x1a\x14.X509BundlesResponse0\x01\x121\n" + + "\fFetchJWTSVID\x12\x0f.JWTSVIDRequest\x1a\x10.JWTSVIDResponse\x12<\n" + + "\x0fFetchJWTBundles\x12\x12.JWTBundlesRequest\x1a\x13.JWTBundlesResponse0\x01\x12D\n" + + "\x0fValidateJWTSVID\x12\x17.ValidateJWTSVIDRequest\x1a\x18.ValidateJWTSVIDResponse\x123\n" + + "\fFetchWITSVID\x12\x0f.WITSVIDRequest\x1a\x10.WITSVIDResponse0\x01\x12<\n" + + "\x0fFetchWITBundles\x12\x12.WITBundlesRequest\x1a\x13.WITBundlesResponse0\x01B?Z=github.com/spiffe/go-spiffe/v2/proto/spiffe/workload;workloadb\x06proto3" + +var ( + file_workload_proto_rawDescOnce sync.Once + file_workload_proto_rawDescData []byte +) + +func file_workload_proto_rawDescGZIP() []byte { + file_workload_proto_rawDescOnce.Do(func() { + file_workload_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_workload_proto_rawDesc), len(file_workload_proto_rawDesc))) + }) + return file_workload_proto_rawDescData +} + +var file_workload_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_workload_proto_goTypes = []any{ + (*X509SVIDRequest)(nil), // 0: X509SVIDRequest + (*X509SVIDResponse)(nil), // 1: X509SVIDResponse + (*X509SVID)(nil), // 2: X509SVID + (*X509BundlesRequest)(nil), // 3: X509BundlesRequest + (*X509BundlesResponse)(nil), // 4: X509BundlesResponse + (*JWTSVIDRequest)(nil), // 5: JWTSVIDRequest + (*JWTSVIDResponse)(nil), // 6: JWTSVIDResponse + (*JWTSVID)(nil), // 7: JWTSVID + (*JWTBundlesRequest)(nil), // 8: JWTBundlesRequest + (*JWTBundlesResponse)(nil), // 9: JWTBundlesResponse + (*ValidateJWTSVIDRequest)(nil), // 10: ValidateJWTSVIDRequest + (*ValidateJWTSVIDResponse)(nil), // 11: ValidateJWTSVIDResponse + (*WITSVIDRequest)(nil), // 12: WITSVIDRequest + (*WITSVIDResponse)(nil), // 13: WITSVIDResponse + (*WITSVID)(nil), // 14: WITSVID + (*WITBundlesRequest)(nil), // 15: WITBundlesRequest + (*WITBundlesResponse)(nil), // 16: WITBundlesResponse + nil, // 17: X509SVIDResponse.FederatedBundlesEntry + nil, // 18: X509BundlesResponse.BundlesEntry + nil, // 19: JWTBundlesResponse.BundlesEntry + nil, // 20: WITBundlesResponse.BundlesEntry + (*structpb.Struct)(nil), // 21: google.protobuf.Struct +} +var file_workload_proto_depIdxs = []int32{ + 2, // 0: X509SVIDResponse.svids:type_name -> X509SVID + 17, // 1: X509SVIDResponse.federated_bundles:type_name -> X509SVIDResponse.FederatedBundlesEntry + 18, // 2: X509BundlesResponse.bundles:type_name -> X509BundlesResponse.BundlesEntry + 7, // 3: JWTSVIDResponse.svids:type_name -> JWTSVID + 19, // 4: JWTBundlesResponse.bundles:type_name -> JWTBundlesResponse.BundlesEntry + 21, // 5: ValidateJWTSVIDResponse.claims:type_name -> google.protobuf.Struct + 14, // 6: WITSVIDResponse.svids:type_name -> WITSVID + 20, // 7: WITBundlesResponse.bundles:type_name -> WITBundlesResponse.BundlesEntry + 0, // 8: SpiffeWorkloadAPI.FetchX509SVID:input_type -> X509SVIDRequest + 3, // 9: SpiffeWorkloadAPI.FetchX509Bundles:input_type -> X509BundlesRequest + 5, // 10: SpiffeWorkloadAPI.FetchJWTSVID:input_type -> JWTSVIDRequest + 8, // 11: SpiffeWorkloadAPI.FetchJWTBundles:input_type -> JWTBundlesRequest + 10, // 12: SpiffeWorkloadAPI.ValidateJWTSVID:input_type -> ValidateJWTSVIDRequest + 12, // 13: SpiffeWorkloadAPI.FetchWITSVID:input_type -> WITSVIDRequest + 15, // 14: SpiffeWorkloadAPI.FetchWITBundles:input_type -> WITBundlesRequest + 1, // 15: SpiffeWorkloadAPI.FetchX509SVID:output_type -> X509SVIDResponse + 4, // 16: SpiffeWorkloadAPI.FetchX509Bundles:output_type -> X509BundlesResponse + 6, // 17: SpiffeWorkloadAPI.FetchJWTSVID:output_type -> JWTSVIDResponse + 9, // 18: SpiffeWorkloadAPI.FetchJWTBundles:output_type -> JWTBundlesResponse + 11, // 19: SpiffeWorkloadAPI.ValidateJWTSVID:output_type -> ValidateJWTSVIDResponse + 13, // 20: SpiffeWorkloadAPI.FetchWITSVID:output_type -> WITSVIDResponse + 16, // 21: SpiffeWorkloadAPI.FetchWITBundles:output_type -> WITBundlesResponse + 15, // [15:22] is the sub-list for method output_type + 8, // [8:15] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_workload_proto_init() } +func file_workload_proto_init() { + if File_workload_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_workload_proto_rawDesc), len(file_workload_proto_rawDesc)), + NumEnums: 0, + NumMessages: 21, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_workload_proto_goTypes, + DependencyIndexes: file_workload_proto_depIdxs, + MessageInfos: file_workload_proto_msgTypes, + }.Build() + File_workload_proto = out.File + file_workload_proto_goTypes = nil + file_workload_proto_depIdxs = nil +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.proto b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.proto new file mode 100644 index 0000000000..7c5a6ec7cc --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload.proto @@ -0,0 +1,221 @@ +syntax = "proto3"; + +import "google/protobuf/struct.proto"; + +service SpiffeWorkloadAPI { + ///////////////////////////////////////////////////////////////////////// + // X509-SVID Profile + ///////////////////////////////////////////////////////////////////////// + + // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, + // as well as related information like trust bundles and CRLs. As this + // information changes, subsequent messages will be streamed from the + // server. + rpc FetchX509SVID(X509SVIDRequest) returns (stream X509SVIDResponse); + + // Fetch trust bundles and CRLs. Useful for clients that only need to + // validate SVIDs without obtaining an SVID for themself. As this + // information changes, subsequent messages will be streamed from the + // server. + rpc FetchX509Bundles(X509BundlesRequest) returns (stream X509BundlesResponse); + + ///////////////////////////////////////////////////////////////////////// + // JWT-SVID Profile + ///////////////////////////////////////////////////////////////////////// + + // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, + // for the requested audience. If an optional SPIFFE ID is requested, only + // the JWT-SVID for that SPIFFE ID is returned. + rpc FetchJWTSVID(JWTSVIDRequest) returns (JWTSVIDResponse); + + // Fetches the JWT bundles, formatted as JWKS documents, keyed by the + // SPIFFE ID of the trust domain. As this information changes, subsequent + // messages will be streamed from the server. + rpc FetchJWTBundles(JWTBundlesRequest) returns (stream JWTBundlesResponse); + + // Validates a JWT-SVID against the requested audience. Returns the SPIFFE + // ID of the JWT-SVID and JWT claims. + rpc ValidateJWTSVID(ValidateJWTSVIDRequest) returns (ValidateJWTSVIDResponse); + + ///////////////////////////////////////////////////////////////////////// + // WIT-SVID Profile + ///////////////////////////////////////////////////////////////////////// + + // Fetch WIT-SVIDs for all SPIFFE identities the workload is entitled to. + // As this information changes, subsequent messages will be streamed from + // the server. + rpc FetchWITSVID(WITSVIDRequest) returns (stream WITSVIDResponse); + + // Fetch WIT bundles, formatted as JWKS documents, keyed by the SPIFFE ID + // of the trust domain. As this information changes, subsequent messages + // will be streamed from the server. + rpc FetchWITBundles(WITBundlesRequest) returns (stream WITBundlesResponse); +} + +// The X509SVIDRequest message conveys parameters for requesting an X.509-SVID. +// There are currently no request parameters. +message X509SVIDRequest { } + +// The X509SVIDResponse message carries X.509-SVIDs and related information, +// including a set of global CRLs and a list of bundles the workload may use +// for federating with foreign trust domains. +message X509SVIDResponse { + // Required. A list of X509SVID messages, each of which includes a single + // X.509-SVID, its private key, and the bundle for the trust domain. + repeated X509SVID svids = 1; + + // Optional. ASN.1 DER encoded certificate revocation lists. + repeated bytes crl = 2; + + // Optional. CA certificate bundles belonging to foreign trust domains that + // the workload should trust, keyed by the SPIFFE ID of the foreign trust + // domain. Bundles are ASN.1 DER encoded. + map federated_bundles = 3; +} + +// The X509SVID message carries a single SVID and all associated information, +// including the X.509 bundle for the trust domain. +message X509SVID { + // Required. The SPIFFE ID of the SVID in this entry + string spiffe_id = 1; + + // Required. ASN.1 DER encoded certificate chain. MAY include + // intermediates, the leaf certificate (or SVID itself) MUST come first. + bytes x509_svid = 2; + + // Required. ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted. + bytes x509_svid_key = 3; + + // Required. ASN.1 DER encoded X.509 bundle for the trust domain. + bytes bundle = 4; + + // Optional. An operator-specified string used to provide guidance on how this + // identity should be used by a workload when more than one SVID is returned. + // For example, `internal` and `external` to indicate an SVID for internal or + // external use, respectively. + string hint = 5; +} + +// The X509BundlesRequest message conveys parameters for requesting X.509 +// bundles. There are currently no such parameters. +message X509BundlesRequest { +} + +// The X509BundlesResponse message carries a set of global CRLs and a map of +// trust bundles the workload should trust. +message X509BundlesResponse { + // Optional. ASN.1 DER encoded certificate revocation lists. + repeated bytes crl = 1; + + // Required. CA certificate bundles belonging to trust domains that the + // workload should trust, keyed by the SPIFFE ID of the trust domain. + // Bundles are ASN.1 DER encoded. + map bundles = 2; +} + +message JWTSVIDRequest { + // Required. The audience(s) the workload intends to authenticate against. + repeated string audience = 1; + + // Optional. The requested SPIFFE ID for the JWT-SVID. If unset, all + // JWT-SVIDs to which the workload is entitled are requested. + string spiffe_id = 2; +} + +// The JWTSVIDResponse message conveys JWT-SVIDs. +message JWTSVIDResponse { + // Required. The list of returned JWT-SVIDs. + repeated JWTSVID svids = 1; +} + +// The JWTSVID message carries the JWT-SVID token and associated metadata. +message JWTSVID { + // Required. The SPIFFE ID of the JWT-SVID. + string spiffe_id = 1; + + // Required. Encoded JWT using JWS Compact Serialization. + string svid = 2; + + // Optional. An operator-specified string used to provide guidance on how this + // identity should be used by a workload when more than one SVID is returned. + // For example, `internal` and `external` to indicate an SVID for internal or + // external use, respectively. + string hint = 3; +} + +// The JWTBundlesRequest message conveys parameters for requesting JWT bundles. +// There are currently no such parameters. +message JWTBundlesRequest { } + +// The JWTBundlesReponse conveys JWT bundles. +message JWTBundlesResponse { + // Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust + // domain. + map bundles = 1; +} + +// The ValidateJWTSVIDRequest message conveys request parameters for +// JWT-SVID validation. +message ValidateJWTSVIDRequest { + // Required. The audience of the validating party. The JWT-SVID must + // contain an audience claim which contains this value in order to + // succesfully validate. + string audience = 1; + + // Required. The JWT-SVID to validate, encoded using JWS Compact + // Serialization. + string svid = 2; +} + +// The ValidateJWTSVIDReponse message conveys the JWT-SVID validation results. +message ValidateJWTSVIDResponse { + // Required. The SPIFFE ID of the validated JWT-SVID. + string spiffe_id = 1; + + // Optional. Arbitrary claims contained within the payload of the validated + // JWT-SVID. + google.protobuf.Struct claims = 2; +} + +// WITSVIDRequest conveys parameters for requesting WIT-SVIDs. +message WITSVIDRequest { + // Optional. The requested SPIFFE ID for the WIT-SVID. If unset, all + // WIT-SVIDs to which the workload is entitled are requested. + string spiffe_id = 1; +} + +// WITSVIDResponse conveys WIT-SVIDs. +message WITSVIDResponse { + // Required. The list of returned WIT-SVIDs. + repeated WITSVID svids = 1; +} + +// WITSVID carries a single WIT-SVID and associated metadata. +message WITSVID { + // Required. The SPIFFE ID of the WIT-SVID. + string spiffe_id = 1; + + // Required. Encoded WIT-SVID using JWS Compact Serialization. + string wit_svid = 2; + + // Required. JWK-encoded private key bound to this WIT-SVID. + string wit_svid_key = 3; + + // Optional. An operator-specified string used to provide guidance on how + // this identity should be used by a workload when more than one SVID is + // returned. + string hint = 4; +} + +// WITBundlesRequest conveys parameters for requesting WIT bundles. +// There are currently no such parameters. +message WITBundlesRequest { } + +// WITBundlesResponse conveys WIT bundles. +message WITBundlesResponse { + // Required. JWK encoded WIT bundles, keyed by the SPIFFE ID of the trust + // domain. + map bundles = 1; +} + +option go_package = "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload;workload"; diff --git a/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload_grpc.pb.go b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload_grpc.pb.go new file mode 100644 index 0000000000..4e03a6aeec --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/spiffe/go-spiffe/v2/proto/spiffe/workload/workload_grpc.pb.go @@ -0,0 +1,409 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.30.2 +// source: workload.proto + +package workload + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SpiffeWorkloadAPI_FetchX509SVID_FullMethodName = "/SpiffeWorkloadAPI/FetchX509SVID" + SpiffeWorkloadAPI_FetchX509Bundles_FullMethodName = "/SpiffeWorkloadAPI/FetchX509Bundles" + SpiffeWorkloadAPI_FetchJWTSVID_FullMethodName = "/SpiffeWorkloadAPI/FetchJWTSVID" + SpiffeWorkloadAPI_FetchJWTBundles_FullMethodName = "/SpiffeWorkloadAPI/FetchJWTBundles" + SpiffeWorkloadAPI_ValidateJWTSVID_FullMethodName = "/SpiffeWorkloadAPI/ValidateJWTSVID" + SpiffeWorkloadAPI_FetchWITSVID_FullMethodName = "/SpiffeWorkloadAPI/FetchWITSVID" + SpiffeWorkloadAPI_FetchWITBundles_FullMethodName = "/SpiffeWorkloadAPI/FetchWITBundles" +) + +// SpiffeWorkloadAPIClient is the client API for SpiffeWorkloadAPI service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SpiffeWorkloadAPIClient interface { + // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, + // as well as related information like trust bundles and CRLs. As this + // information changes, subsequent messages will be streamed from the + // server. + FetchX509SVID(ctx context.Context, in *X509SVIDRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[X509SVIDResponse], error) + // Fetch trust bundles and CRLs. Useful for clients that only need to + // validate SVIDs without obtaining an SVID for themself. As this + // information changes, subsequent messages will be streamed from the + // server. + FetchX509Bundles(ctx context.Context, in *X509BundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[X509BundlesResponse], error) + // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, + // for the requested audience. If an optional SPIFFE ID is requested, only + // the JWT-SVID for that SPIFFE ID is returned. + FetchJWTSVID(ctx context.Context, in *JWTSVIDRequest, opts ...grpc.CallOption) (*JWTSVIDResponse, error) + // Fetches the JWT bundles, formatted as JWKS documents, keyed by the + // SPIFFE ID of the trust domain. As this information changes, subsequent + // messages will be streamed from the server. + FetchJWTBundles(ctx context.Context, in *JWTBundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JWTBundlesResponse], error) + // Validates a JWT-SVID against the requested audience. Returns the SPIFFE + // ID of the JWT-SVID and JWT claims. + ValidateJWTSVID(ctx context.Context, in *ValidateJWTSVIDRequest, opts ...grpc.CallOption) (*ValidateJWTSVIDResponse, error) + // Fetch WIT-SVIDs for all SPIFFE identities the workload is entitled to. + // As this information changes, subsequent messages will be streamed from + // the server. + FetchWITSVID(ctx context.Context, in *WITSVIDRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[WITSVIDResponse], error) + // Fetch WIT bundles, formatted as JWKS documents, keyed by the SPIFFE ID + // of the trust domain. As this information changes, subsequent messages + // will be streamed from the server. + FetchWITBundles(ctx context.Context, in *WITBundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[WITBundlesResponse], error) +} + +type spiffeWorkloadAPIClient struct { + cc grpc.ClientConnInterface +} + +func NewSpiffeWorkloadAPIClient(cc grpc.ClientConnInterface) SpiffeWorkloadAPIClient { + return &spiffeWorkloadAPIClient{cc} +} + +func (c *spiffeWorkloadAPIClient) FetchX509SVID(ctx context.Context, in *X509SVIDRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[X509SVIDResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpiffeWorkloadAPI_ServiceDesc.Streams[0], SpiffeWorkloadAPI_FetchX509SVID_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[X509SVIDRequest, X509SVIDResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchX509SVIDClient = grpc.ServerStreamingClient[X509SVIDResponse] + +func (c *spiffeWorkloadAPIClient) FetchX509Bundles(ctx context.Context, in *X509BundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[X509BundlesResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpiffeWorkloadAPI_ServiceDesc.Streams[1], SpiffeWorkloadAPI_FetchX509Bundles_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[X509BundlesRequest, X509BundlesResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchX509BundlesClient = grpc.ServerStreamingClient[X509BundlesResponse] + +func (c *spiffeWorkloadAPIClient) FetchJWTSVID(ctx context.Context, in *JWTSVIDRequest, opts ...grpc.CallOption) (*JWTSVIDResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(JWTSVIDResponse) + err := c.cc.Invoke(ctx, SpiffeWorkloadAPI_FetchJWTSVID_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *spiffeWorkloadAPIClient) FetchJWTBundles(ctx context.Context, in *JWTBundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JWTBundlesResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpiffeWorkloadAPI_ServiceDesc.Streams[2], SpiffeWorkloadAPI_FetchJWTBundles_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[JWTBundlesRequest, JWTBundlesResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchJWTBundlesClient = grpc.ServerStreamingClient[JWTBundlesResponse] + +func (c *spiffeWorkloadAPIClient) ValidateJWTSVID(ctx context.Context, in *ValidateJWTSVIDRequest, opts ...grpc.CallOption) (*ValidateJWTSVIDResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateJWTSVIDResponse) + err := c.cc.Invoke(ctx, SpiffeWorkloadAPI_ValidateJWTSVID_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *spiffeWorkloadAPIClient) FetchWITSVID(ctx context.Context, in *WITSVIDRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[WITSVIDResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpiffeWorkloadAPI_ServiceDesc.Streams[3], SpiffeWorkloadAPI_FetchWITSVID_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WITSVIDRequest, WITSVIDResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchWITSVIDClient = grpc.ServerStreamingClient[WITSVIDResponse] + +func (c *spiffeWorkloadAPIClient) FetchWITBundles(ctx context.Context, in *WITBundlesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[WITBundlesResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpiffeWorkloadAPI_ServiceDesc.Streams[4], SpiffeWorkloadAPI_FetchWITBundles_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WITBundlesRequest, WITBundlesResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchWITBundlesClient = grpc.ServerStreamingClient[WITBundlesResponse] + +// SpiffeWorkloadAPIServer is the server API for SpiffeWorkloadAPI service. +// All implementations must embed UnimplementedSpiffeWorkloadAPIServer +// for forward compatibility. +type SpiffeWorkloadAPIServer interface { + // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, + // as well as related information like trust bundles and CRLs. As this + // information changes, subsequent messages will be streamed from the + // server. + FetchX509SVID(*X509SVIDRequest, grpc.ServerStreamingServer[X509SVIDResponse]) error + // Fetch trust bundles and CRLs. Useful for clients that only need to + // validate SVIDs without obtaining an SVID for themself. As this + // information changes, subsequent messages will be streamed from the + // server. + FetchX509Bundles(*X509BundlesRequest, grpc.ServerStreamingServer[X509BundlesResponse]) error + // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, + // for the requested audience. If an optional SPIFFE ID is requested, only + // the JWT-SVID for that SPIFFE ID is returned. + FetchJWTSVID(context.Context, *JWTSVIDRequest) (*JWTSVIDResponse, error) + // Fetches the JWT bundles, formatted as JWKS documents, keyed by the + // SPIFFE ID of the trust domain. As this information changes, subsequent + // messages will be streamed from the server. + FetchJWTBundles(*JWTBundlesRequest, grpc.ServerStreamingServer[JWTBundlesResponse]) error + // Validates a JWT-SVID against the requested audience. Returns the SPIFFE + // ID of the JWT-SVID and JWT claims. + ValidateJWTSVID(context.Context, *ValidateJWTSVIDRequest) (*ValidateJWTSVIDResponse, error) + // Fetch WIT-SVIDs for all SPIFFE identities the workload is entitled to. + // As this information changes, subsequent messages will be streamed from + // the server. + FetchWITSVID(*WITSVIDRequest, grpc.ServerStreamingServer[WITSVIDResponse]) error + // Fetch WIT bundles, formatted as JWKS documents, keyed by the SPIFFE ID + // of the trust domain. As this information changes, subsequent messages + // will be streamed from the server. + FetchWITBundles(*WITBundlesRequest, grpc.ServerStreamingServer[WITBundlesResponse]) error + mustEmbedUnimplementedSpiffeWorkloadAPIServer() +} + +// UnimplementedSpiffeWorkloadAPIServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSpiffeWorkloadAPIServer struct{} + +func (UnimplementedSpiffeWorkloadAPIServer) FetchX509SVID(*X509SVIDRequest, grpc.ServerStreamingServer[X509SVIDResponse]) error { + return status.Errorf(codes.Unimplemented, "method FetchX509SVID not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) FetchX509Bundles(*X509BundlesRequest, grpc.ServerStreamingServer[X509BundlesResponse]) error { + return status.Errorf(codes.Unimplemented, "method FetchX509Bundles not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) FetchJWTSVID(context.Context, *JWTSVIDRequest) (*JWTSVIDResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchJWTSVID not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) FetchJWTBundles(*JWTBundlesRequest, grpc.ServerStreamingServer[JWTBundlesResponse]) error { + return status.Errorf(codes.Unimplemented, "method FetchJWTBundles not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) ValidateJWTSVID(context.Context, *ValidateJWTSVIDRequest) (*ValidateJWTSVIDResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateJWTSVID not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) FetchWITSVID(*WITSVIDRequest, grpc.ServerStreamingServer[WITSVIDResponse]) error { + return status.Errorf(codes.Unimplemented, "method FetchWITSVID not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) FetchWITBundles(*WITBundlesRequest, grpc.ServerStreamingServer[WITBundlesResponse]) error { + return status.Errorf(codes.Unimplemented, "method FetchWITBundles not implemented") +} +func (UnimplementedSpiffeWorkloadAPIServer) mustEmbedUnimplementedSpiffeWorkloadAPIServer() {} +func (UnimplementedSpiffeWorkloadAPIServer) testEmbeddedByValue() {} + +// UnsafeSpiffeWorkloadAPIServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SpiffeWorkloadAPIServer will +// result in compilation errors. +type UnsafeSpiffeWorkloadAPIServer interface { + mustEmbedUnimplementedSpiffeWorkloadAPIServer() +} + +func RegisterSpiffeWorkloadAPIServer(s grpc.ServiceRegistrar, srv SpiffeWorkloadAPIServer) { + // If the following call pancis, it indicates UnimplementedSpiffeWorkloadAPIServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SpiffeWorkloadAPI_ServiceDesc, srv) +} + +func _SpiffeWorkloadAPI_FetchX509SVID_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(X509SVIDRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpiffeWorkloadAPIServer).FetchX509SVID(m, &grpc.GenericServerStream[X509SVIDRequest, X509SVIDResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchX509SVIDServer = grpc.ServerStreamingServer[X509SVIDResponse] + +func _SpiffeWorkloadAPI_FetchX509Bundles_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(X509BundlesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpiffeWorkloadAPIServer).FetchX509Bundles(m, &grpc.GenericServerStream[X509BundlesRequest, X509BundlesResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchX509BundlesServer = grpc.ServerStreamingServer[X509BundlesResponse] + +func _SpiffeWorkloadAPI_FetchJWTSVID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(JWTSVIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SpiffeWorkloadAPIServer).FetchJWTSVID(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SpiffeWorkloadAPI_FetchJWTSVID_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SpiffeWorkloadAPIServer).FetchJWTSVID(ctx, req.(*JWTSVIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SpiffeWorkloadAPI_FetchJWTBundles_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(JWTBundlesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpiffeWorkloadAPIServer).FetchJWTBundles(m, &grpc.GenericServerStream[JWTBundlesRequest, JWTBundlesResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchJWTBundlesServer = grpc.ServerStreamingServer[JWTBundlesResponse] + +func _SpiffeWorkloadAPI_ValidateJWTSVID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateJWTSVIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SpiffeWorkloadAPIServer).ValidateJWTSVID(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SpiffeWorkloadAPI_ValidateJWTSVID_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SpiffeWorkloadAPIServer).ValidateJWTSVID(ctx, req.(*ValidateJWTSVIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SpiffeWorkloadAPI_FetchWITSVID_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WITSVIDRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpiffeWorkloadAPIServer).FetchWITSVID(m, &grpc.GenericServerStream[WITSVIDRequest, WITSVIDResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchWITSVIDServer = grpc.ServerStreamingServer[WITSVIDResponse] + +func _SpiffeWorkloadAPI_FetchWITBundles_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WITBundlesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpiffeWorkloadAPIServer).FetchWITBundles(m, &grpc.GenericServerStream[WITBundlesRequest, WITBundlesResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpiffeWorkloadAPI_FetchWITBundlesServer = grpc.ServerStreamingServer[WITBundlesResponse] + +// SpiffeWorkloadAPI_ServiceDesc is the grpc.ServiceDesc for SpiffeWorkloadAPI service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SpiffeWorkloadAPI_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "SpiffeWorkloadAPI", + HandlerType: (*SpiffeWorkloadAPIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "FetchJWTSVID", + Handler: _SpiffeWorkloadAPI_FetchJWTSVID_Handler, + }, + { + MethodName: "ValidateJWTSVID", + Handler: _SpiffeWorkloadAPI_ValidateJWTSVID_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "FetchX509SVID", + Handler: _SpiffeWorkloadAPI_FetchX509SVID_Handler, + ServerStreams: true, + }, + { + StreamName: "FetchX509Bundles", + Handler: _SpiffeWorkloadAPI_FetchX509Bundles_Handler, + ServerStreams: true, + }, + { + StreamName: "FetchJWTBundles", + Handler: _SpiffeWorkloadAPI_FetchJWTBundles_Handler, + ServerStreams: true, + }, + { + StreamName: "FetchWITSVID", + Handler: _SpiffeWorkloadAPI_FetchWITSVID_Handler, + ServerStreams: true, + }, + { + StreamName: "FetchWITBundles", + Handler: _SpiffeWorkloadAPI_FetchWITBundles_Handler, + ServerStreams: true, + }, + }, + Metadata: "workload.proto", +} diff --git a/src/code.cloudfoundry.org/vendor/modules.txt b/src/code.cloudfoundry.org/vendor/modules.txt index 6f318c4363..9e2edfbb91 100644 --- a/src/code.cloudfoundry.org/vendor/modules.txt +++ b/src/code.cloudfoundry.org/vendor/modules.txt @@ -9,14 +9,26 @@ code.cloudfoundry.org/archiver/extractor/test_helper # code.cloudfoundry.org/bbs v1.6.0 ## explicit; go 1.26.2 code.cloudfoundry.org/bbs +code.cloudfoundry.org/bbs/cmd/bbs code.cloudfoundry.org/bbs/cmd/bbs/config code.cloudfoundry.org/bbs/cmd/bbs/testrunner +code.cloudfoundry.org/bbs/controllers +code.cloudfoundry.org/bbs/converger +code.cloudfoundry.org/bbs/db +code.cloudfoundry.org/bbs/db/migrations +code.cloudfoundry.org/bbs/db/sqldb code.cloudfoundry.org/bbs/db/sqldb/helpers +code.cloudfoundry.org/bbs/encryptor code.cloudfoundry.org/bbs/events +code.cloudfoundry.org/bbs/events/calculator code.cloudfoundry.org/bbs/events/eventfakes code.cloudfoundry.org/bbs/fake_bbs +code.cloudfoundry.org/bbs/handlers code.cloudfoundry.org/bbs/handlers/middleware +code.cloudfoundry.org/bbs/metrics +code.cloudfoundry.org/bbs/migration code.cloudfoundry.org/bbs/serviceclient +code.cloudfoundry.org/bbs/taskworkpool code.cloudfoundry.org/bbs/test_helpers code.cloudfoundry.org/bbs/test_helpers/sqlrunner code.cloudfoundry.org/bbs/trace @@ -30,6 +42,9 @@ code.cloudfoundry.org/bbs/format ## explicit; go 1.26.2 code.cloudfoundry.org/bbs/models code.cloudfoundry.org/bbs/models/test/model_helpers +# code.cloudfoundry.org/buildpackapplifecycle v0.0.0-20260504201830-3e265382f635 +## explicit +code.cloudfoundry.org/buildpackapplifecycle/databaseuri # code.cloudfoundry.org/bytefmt v0.78.0 ## explicit; go 1.25.0 code.cloudfoundry.org/bytefmt @@ -66,6 +81,7 @@ code.cloudfoundry.org/credhub-cli/credhub/server code.cloudfoundry.org/debugserver # code.cloudfoundry.org/diego-db-helpers v0.5.0 ## explicit; go 1.26.0 +code.cloudfoundry.org/diego-db-helpers/guidprovider code.cloudfoundry.org/diego-db-helpers/sqldb/helpers code.cloudfoundry.org/diego-db-helpers/sqldb/helpers/monitor code.cloudfoundry.org/diego-db-helpers/testhelpers/sqlrunner @@ -138,12 +154,18 @@ code.cloudfoundry.org/localip # code.cloudfoundry.org/locket v1.3.0 ## explicit; go 1.26.0 code.cloudfoundry.org/locket +code.cloudfoundry.org/locket/cmd/locket code.cloudfoundry.org/locket/cmd/locket/certauthority code.cloudfoundry.org/locket/cmd/locket/config code.cloudfoundry.org/locket/cmd/locket/testrunner +code.cloudfoundry.org/locket/db +code.cloudfoundry.org/locket/expiration +code.cloudfoundry.org/locket/grpcserver +code.cloudfoundry.org/locket/handlers code.cloudfoundry.org/locket/jointlock code.cloudfoundry.org/locket/lock code.cloudfoundry.org/locket/lockheldmetrics +code.cloudfoundry.org/locket/metrics code.cloudfoundry.org/locket/metrics/helpers code.cloudfoundry.org/locket/metrics/helpers/helpersfakes code.cloudfoundry.org/locket/models @@ -157,6 +179,11 @@ code.cloudfoundry.org/routing-api/models code.cloudfoundry.org/routing-api/trace code.cloudfoundry.org/routing-api/uaaclient code.cloudfoundry.org/routing-api/uaaclient/fakes +# code.cloudfoundry.org/routing-info v0.1.0 +## explicit; go 1.26 +code.cloudfoundry.org/routing-info/cfroutes +code.cloudfoundry.org/routing-info/internalroutes +code.cloudfoundry.org/routing-info/tcp_routes # code.cloudfoundry.org/tlsconfig v0.60.0 ## explicit; go 1.25.8 code.cloudfoundry.org/tlsconfig @@ -763,6 +790,9 @@ github.com/spf13/cobra # github.com/spf13/pflag v1.0.10 ## explicit; go 1.12 github.com/spf13/pflag +# github.com/spiffe/go-spiffe/v2 v2.8.1 +## explicit; go 1.24.0 +github.com/spiffe/go-spiffe/v2/proto/spiffe/workload # github.com/square/certstrap v1.3.0 ## explicit; go 1.18 github.com/square/certstrap/pkix