diff --git a/api/go.mod b/api/go.mod index a74a4dd4571b2..6d69900733e2f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,6 +1,6 @@ module github.com/gravitational/teleport/api -go 1.22.0 +go 1.22.7 require ( github.com/coreos/go-semver v0.3.1 @@ -27,7 +27,7 @@ require ( golang.org/x/net v0.31.0 golang.org/x/term v0.26.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.66.3 + google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/api/go.sum b/api/go.sum index 1a13d733519ab..40ba92bdc3a95 100644 --- a/api/go.sum +++ b/api/go.sum @@ -728,6 +728,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -1543,8 +1545,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/go.mod b/go.mod index 4f6db64e6cd4a..b580823686695 100644 --- a/go.mod +++ b/go.mod @@ -210,7 +210,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/api v0.197.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.66.3 + google.golang.org/grpc v1.68.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.35.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c @@ -238,7 +238,7 @@ require ( ) require ( - cel.dev/expr v0.16.0 // indirect + cel.dev/expr v0.16.1 // indirect cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect @@ -291,7 +291,7 @@ require ( github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect - github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/containerd v1.7.23 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/go.sum b/go.sum index 6a61c84319046..fb99322c67f81 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= +cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -1039,8 +1039,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 h1:fLZ97KE86ELjEYJCEUVzmbhfzDxHHGwBrDVMd4XL6Bs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= @@ -3049,8 +3049,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod index c81de13640ba4..1db51b1203e49 100644 --- a/integrations/event-handler/go.mod +++ b/integrations/event-handler/go.mod @@ -294,7 +294,7 @@ require ( google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.3 // indirect + google.golang.org/grpc v1.68.0 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum index 060344c2aa64c..8f76fbccfeabe 100644 --- a/integrations/event-handler/go.sum +++ b/integrations/event-handler/go.sum @@ -2312,8 +2312,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index 3529be4eecf52..46cdc90197d5d 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -23,7 +23,7 @@ require ( github.com/jonboulle/clockwork v0.4.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.66.3 + google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.35.2 ) @@ -118,7 +118,7 @@ require ( github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/containerd v1.7.23 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 1dfa25f86cd0a..2dacd8c9840a2 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= +cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -931,8 +931,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 h1:fLZ97KE86ELjEYJCEUVzmbhfzDxHHGwBrDVMd4XL6Bs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ= @@ -2671,8 +2671,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= diff --git a/lib/auth/transport_credentials_test.go b/lib/auth/transport_credentials_test.go index 602eed0ddb2dd..c0a4519d51dcb 100644 --- a/lib/auth/transport_credentials_test.go +++ b/lib/auth/transport_credentials_test.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "io" "net" + "slices" "testing" "time" @@ -286,7 +287,17 @@ func TestTransportCredentials_ServerHandshake(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { require.NoError(t, conn.Close()) }) - clientConn := tls.Client(conn, test.clientTLSConf) + // this would be done by the grpc TransportCredential in the grpc + // client, but we're going to fake it with just a tls.Client, so we + // have to add the http2 next proto ourselves (enforced by grpc-go + // starting from v1.67, and required by the http2 spec when speaking + // http2 in TLS) + clientTLSConf := test.clientTLSConf + if !slices.Contains(clientTLSConf.NextProtos, "h2") { + clientTLSConf = clientTLSConf.Clone() + clientTLSConf.NextProtos = append(clientTLSConf.NextProtos, "h2") + } + clientConn := tls.Client(conn, clientTLSConf) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() diff --git a/lib/autoupdate/agent/installer.go b/lib/autoupdate/agent/installer.go index 84c41bd54f235..41db748a3c1b8 100644 --- a/lib/autoupdate/agent/installer.go +++ b/lib/autoupdate/agent/installer.go @@ -59,12 +59,8 @@ const ( const ( // serviceDir contains the relative path to the Teleport SystemD service dir. serviceDir = "lib/systemd/system" - // serviceName contains the name of the Teleport SystemD service file. + // serviceName contains the upstream name of the Teleport SystemD service file. serviceName = "teleport.service" - // updateServiceName contains the name of the Teleport Update Systemd service - updateServiceName = "teleport-update.service" - // updateTimerName contains the name of the Teleport Update Systemd timer - updateTimerName = "teleport-update.timer" ) // LocalInstaller manages the creation and removal of installations @@ -74,12 +70,12 @@ type LocalInstaller struct { InstallDir string // LinkBinDir contains symlinks to the linked installation's binaries. LinkBinDir string - // LinkServiceDir contains a copy of the linked installation's systemd service. - LinkServiceDir string + // CopyServiceFile contains a copy of the linked installation's systemd service. + CopyServiceFile string // SystemBinDir contains binaries for the system (packaged) install of Teleport. SystemBinDir string - // SystemServiceDir contains the systemd service file for the system (packaged) install of Teleport. - SystemServiceDir string + // SystemServiceFile contains the systemd service file for the system (packaged) install of Teleport. + SystemServiceFile string // HTTP is an HTTP client for downloading Teleport. HTTP *http.Client // Log contains a logger. @@ -88,6 +84,8 @@ type LocalInstaller struct { ReservedFreeTmpDisk uint64 // ReservedFreeInstallDisk is the amount of disk that must remain free in the install directory. ReservedFreeInstallDisk uint64 + // TransformService transforms the systemd service during copying. + TransformService func([]byte) []byte } // Remove a Teleport version directory from InstallDir. @@ -356,7 +354,7 @@ func (li *LocalInstaller) extract(ctx context.Context, dstDir string, src io.Rea } zr, err := gzip.NewReader(src) if err != nil { - return trace.Errorf("requires gzip-compressed body: %v", err) + return trace.Errorf("requires gzip-compressed body: %w", err) } li.Log.InfoContext(ctx, "Extracting Teleport tarball.", "path", dstDir, "size", max) @@ -417,7 +415,7 @@ func (li *LocalInstaller) List(ctx context.Context) (versions []string, err erro return versions, nil } -// Link the specified version into the system LinkBinDir and LinkServiceDir. +// Link the specified version into the system LinkBinDir and CopyServiceFile. // The revert function restores the previous linking. // See Installer interface for additional specs. func (li *LocalInstaller) Link(ctx context.Context, version string) (revert func(context.Context) bool, err error) { @@ -428,7 +426,7 @@ func (li *LocalInstaller) Link(ctx context.Context, version string) (revert func } revert, err = li.forceLinks(ctx, filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir), + filepath.Join(versionDir, serviceDir, serviceName), ) if err != nil { return revert, trace.Wrap(err) @@ -436,11 +434,11 @@ func (li *LocalInstaller) Link(ctx context.Context, version string) (revert func return revert, nil } -// LinkSystem links the system (package) version into LinkBinDir and LinkServiceDir. +// LinkSystem links the system (package) version into LinkBinDir and CopyServiceFile. // The revert function restores the previous linking. // See Installer interface for additional specs. func (li *LocalInstaller) LinkSystem(ctx context.Context) (revert func(context.Context) bool, err error) { - revert, err = li.forceLinks(ctx, li.SystemBinDir, li.SystemServiceDir) + revert, err = li.forceLinks(ctx, li.SystemBinDir, li.SystemServiceFile) return revert, trace.Wrap(err) } @@ -454,7 +452,7 @@ func (li *LocalInstaller) TryLink(ctx context.Context, version string) error { } return trace.Wrap(li.tryLinks(ctx, filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir), + filepath.Join(versionDir, serviceDir, serviceName), )) } @@ -462,10 +460,10 @@ func (li *LocalInstaller) TryLink(ctx context.Context, version string) error { // no installation of Teleport is already linked or partially linked. // See Installer interface for additional specs. func (li *LocalInstaller) TryLinkSystem(ctx context.Context) error { - return trace.Wrap(li.tryLinks(ctx, li.SystemBinDir, li.SystemServiceDir)) + return trace.Wrap(li.tryLinks(ctx, li.SystemBinDir, li.SystemServiceFile)) } -// Unlink unlinks a version from LinkBinDir and LinkServiceDir. +// Unlink unlinks a version from LinkBinDir and CopyServiceFile. // See Installer interface for additional specs. func (li *LocalInstaller) Unlink(ctx context.Context, version string) error { versionDir, err := li.versionDir(version) @@ -474,14 +472,14 @@ func (li *LocalInstaller) Unlink(ctx context.Context, version string) error { } return trace.Wrap(li.removeLinks(ctx, filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir), + filepath.Join(versionDir, serviceDir, serviceName), )) } -// UnlinkSystem unlinks the system (package) version from LinkBinDir and LinkServiceDir. +// UnlinkSystem unlinks the system (package) version from LinkBinDir and CopyServiceFile. // See Installer interface for additional specs. func (li *LocalInstaller) UnlinkSystem(ctx context.Context) error { - return trace.Wrap(li.removeLinks(ctx, li.SystemBinDir, li.SystemServiceDir)) + return trace.Wrap(li.removeLinks(ctx, li.SystemBinDir, li.SystemServiceFile)) } // symlink from oldname to newname @@ -501,7 +499,7 @@ type smallFile struct { // forceLinks will revert any overridden links or files if it hits an error. // If successful, forceLinks may also be reverted after it returns by calling revert. // The revert function returns true if reverting succeeds. -func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcDir string) (revert func(context.Context) bool, err error) { +func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcPath string) (revert func(context.Context) bool, err error) { // setup revert function var ( revertLinks []symlink @@ -544,7 +542,7 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcDir string) if err != nil { return revert, trace.Wrap(err) } - err = os.MkdirAll(li.LinkServiceDir, systemDirMode) + err = os.MkdirAll(filepath.Dir(li.CopyServiceFile), systemDirMode) if err != nil { return revert, trace.Wrap(err) } @@ -580,11 +578,9 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcDir string) // create systemd service file - src := filepath.Join(svcDir, serviceName) - dst := filepath.Join(li.LinkServiceDir, serviceName) - orig, err := forceCopy(dst, src, maxServiceFileSize) + orig, err := li.forceCopyService(li.CopyServiceFile, svcPath, maxServiceFileSize) if err != nil && !errors.Is(err, os.ErrExist) { - return revert, trace.Errorf("failed to write file %s: %w", serviceName, err) + return revert, trace.Errorf("failed to copy service: %w", err) } if orig != nil { revertFiles = append(revertFiles, *orig) @@ -592,6 +588,17 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcDir string) return revert, nil } +// forceCopyService uses forceCopy to copy a systemd service file from src to dst. +// The contents of both src and dst must be smaller than n. +// See forceCopy for more details. +func (li *LocalInstaller) forceCopyService(dst, src string, n int64) (orig *smallFile, err error) { + srcData, err := readFileN(src, n) + if err != nil { + return nil, trace.Wrap(err) + } + return forceCopy(dst, li.TransformService(srcData), n) +} + // forceLink attempts to create a symlink, atomically replacing an existing link if already present. // If a non-symlink file or directory exists in newname already, forceLink errors. // If the link is already present with the desired oldname, forceLink returns os.ErrExist. @@ -633,16 +640,12 @@ func isExecutable(path string) (bool, error) { fi.Mode()&0111 == 0111, nil } -// forceCopy atomically copies a file from src to dst, replacing an existing file at dst if needed. -// Both src and dst must be smaller than n. +// forceCopy atomically copies a file from srcData to dst, replacing an existing file at dst if needed. +// The contents of dst must be smaller than n. // forceCopy returns the original file path, mode, and contents as orig. -// If an irregular file, too large file, or directory exists in path already, forceCopy errors. +// If an irregular file, too large file, or directory exists in dst already, forceCopy errors. // If the file is already present with the desired contents, forceCopy returns os.ErrExist. -func forceCopy(dst, src string, n int64) (orig *smallFile, err error) { - srcData, err := readFileN(src, n) - if err != nil { - return nil, trace.Wrap(err) - } +func forceCopy(dst string, srcData []byte, n int64) (orig *smallFile, err error) { fi, err := os.Lstat(dst) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, trace.Wrap(err) @@ -681,7 +684,7 @@ func readFileN(name string, n int64) ([]byte, error) { return data, trace.Wrap(err) } -func (li *LocalInstaller) removeLinks(ctx context.Context, binDir, svcDir string) error { +func (li *LocalInstaller) removeLinks(ctx context.Context, binDir, svcPath string) error { removeService := false entries, err := os.ReadDir(binDir) if err != nil { @@ -720,26 +723,24 @@ func (li *LocalInstaller) removeLinks(ctx context.Context, binDir, svcDir string li.Log.DebugContext(ctx, "Teleport binary not unlinked. Skipping removal of teleport.service.") return nil } - src := filepath.Join(svcDir, serviceName) - srcBytes, err := readFileN(src, maxServiceFileSize) + srcBytes, err := readFileN(svcPath, maxServiceFileSize) if err != nil { return trace.Wrap(err) } - dst := filepath.Join(li.LinkServiceDir, serviceName) - dstBytes, err := readFileN(dst, maxServiceFileSize) + dstBytes, err := readFileN(li.CopyServiceFile, maxServiceFileSize) if errors.Is(err, os.ErrNotExist) { - li.Log.DebugContext(ctx, "Service not present.", "path", dst) + li.Log.DebugContext(ctx, "Service not present.", "path", li.CopyServiceFile) return nil } if err != nil { return trace.Wrap(err) } - if !bytes.Equal(srcBytes, dstBytes) { + if !bytes.Equal(li.TransformService(srcBytes), dstBytes) { li.Log.WarnContext(ctx, "Removed teleport binary link, but skipping removal of custom teleport.service: the service file does not match the reference file for this version. The file might have been manually edited.") return nil } - if err := os.Remove(dst); err != nil { - return trace.Errorf("error removing copy of %s: %w", filepath.Base(dst), err) + if err := os.Remove(li.CopyServiceFile); err != nil { + return trace.Errorf("error removing copy of %s: %w", filepath.Base(li.CopyServiceFile), err) } return nil } @@ -748,13 +749,13 @@ func (li *LocalInstaller) removeLinks(ctx context.Context, binDir, svcDir string // Existing links that point to files outside binDir or svcDir, as well as existing non-link files, will error. // tryLinks will not attempt to create any links if linking could result in an error. // However, concurrent changes to links may result in an error with partially-complete linking. -func (li *LocalInstaller) tryLinks(ctx context.Context, binDir, svcDir string) error { +func (li *LocalInstaller) tryLinks(ctx context.Context, binDir, svcPath string) error { // ensure target directories exist before trying to create links err := os.MkdirAll(li.LinkBinDir, systemDirMode) if err != nil { return trace.Wrap(err) } - err = os.MkdirAll(li.LinkServiceDir, systemDirMode) + err = os.MkdirAll(filepath.Dir(li.CopyServiceFile), systemDirMode) if err != nil { return trace.Wrap(err) } @@ -795,11 +796,9 @@ func (li *LocalInstaller) tryLinks(ctx context.Context, binDir, svcDir string) e } // if any binaries are linked from binDir, always link the service from svcDir - src := filepath.Join(svcDir, serviceName) - dst := filepath.Join(li.LinkServiceDir, serviceName) - _, err = forceCopy(dst, src, maxServiceFileSize) + _, err = li.forceCopyService(li.CopyServiceFile, svcPath, maxServiceFileSize) if err != nil && !errors.Is(err, os.ErrExist) { - return trace.Errorf("error writing %s: %w", serviceName, err) + return trace.Errorf("failed to copy service: %w", err) } return nil diff --git a/lib/autoupdate/agent/installer_test.go b/lib/autoupdate/agent/installer_test.go index 9b9c9b268490e..22c983fdbfeb0 100644 --- a/lib/autoupdate/agent/installer_test.go +++ b/lib/autoupdate/agent/installer_test.go @@ -204,8 +204,9 @@ func TestLocalInstaller_Link(t *testing.T) { existingLinks []string existingFiles []string - resultPaths []string - errMatch string + resultLinks []string + resultServices []string + errMatch string }{ { name: "present with new links", @@ -226,10 +227,12 @@ func TestLocalInstaller_Link(t *testing.T) { }, installFileMode: os.ModePerm, - resultPaths: []string{ + resultLinks: []string{ "bin/teleport", "bin/tsh", "bin/tbot", + }, + resultServices: []string{ "lib/systemd/system/teleport.service", }, }, @@ -281,10 +284,12 @@ func TestLocalInstaller_Link(t *testing.T) { "lib/systemd/system/teleport.service", }, - resultPaths: []string{ + resultLinks: []string{ "bin/teleport", "bin/tsh", "bin/tbot", + }, + resultServices: []string{ "lib/systemd/system/teleport.service", }, }, @@ -392,10 +397,13 @@ func TestLocalInstaller_Link(t *testing.T) { } installer := &LocalInstaller{ - InstallDir: versionsDir, - LinkBinDir: filepath.Join(linkDir, "bin"), - LinkServiceDir: filepath.Join(linkDir, serviceDir), - Log: slog.Default(), + InstallDir: versionsDir, + LinkBinDir: filepath.Join(linkDir, "bin"), + CopyServiceFile: filepath.Join(linkDir, serviceDir, serviceName), + Log: slog.Default(), + TransformService: func(b []byte) []byte { + return []byte("[transform]" + string(b)) + }, } ctx := context.Background() revert, err := installer.Link(ctx, version) @@ -423,11 +431,16 @@ func TestLocalInstaller_Link(t *testing.T) { require.NoError(t, err) // verify links - for _, link := range tt.resultPaths { + for _, link := range tt.resultLinks { v, err := os.ReadFile(filepath.Join(linkDir, link)) require.NoError(t, err) require.Equal(t, filepath.Base(link), string(v)) } + for _, svc := range tt.resultServices { + v, err := os.ReadFile(filepath.Join(linkDir, svc)) + require.NoError(t, err) + require.Equal(t, "[transform]"+filepath.Base(svc), string(v)) + } // verify manual revert ok := revert(ctx) @@ -459,8 +472,9 @@ func TestLocalInstaller_TryLink(t *testing.T) { existingLinks []string existingFiles []string - resultPaths []string - errMatch string + resultLinks []string + resultServices []string + errMatch string }{ { name: "present with new links", @@ -481,10 +495,12 @@ func TestLocalInstaller_TryLink(t *testing.T) { }, installFileMode: os.ModePerm, - resultPaths: []string{ + resultLinks: []string{ "bin/teleport", "bin/tsh", "bin/tbot", + }, + resultServices: []string{ "lib/systemd/system/teleport.service", }, }, @@ -634,10 +650,13 @@ func TestLocalInstaller_TryLink(t *testing.T) { } installer := &LocalInstaller{ - InstallDir: versionsDir, - LinkBinDir: filepath.Join(linkDir, "bin"), - LinkServiceDir: filepath.Join(linkDir, serviceDir), - Log: slog.Default(), + InstallDir: versionsDir, + LinkBinDir: filepath.Join(linkDir, "bin"), + CopyServiceFile: filepath.Join(linkDir, serviceDir, serviceName), + Log: slog.Default(), + TransformService: func(b []byte) []byte { + return []byte("[transform]" + string(b)) + }, } ctx := context.Background() err = installer.TryLink(ctx, version) @@ -661,11 +680,17 @@ func TestLocalInstaller_TryLink(t *testing.T) { require.NoError(t, err) // verify links - for _, link := range tt.resultPaths { + for _, link := range tt.resultLinks { v, err := os.ReadFile(filepath.Join(linkDir, link)) require.NoError(t, err) require.Equal(t, filepath.Base(link), string(v)) } + for _, svc := range tt.resultServices { + v, err := os.ReadFile(filepath.Join(linkDir, svc)) + require.NoError(t, err) + require.Equal(t, "[transform]"+filepath.Base(svc), string(v)) + } + }) } } @@ -773,10 +798,13 @@ func TestLocalInstaller_Remove(t *testing.T) { linkDir := t.TempDir() installer := &LocalInstaller{ - InstallDir: versionsDir, - LinkBinDir: filepath.Join(linkDir, "bin"), - LinkServiceDir: filepath.Join(linkDir, serviceDir), - Log: slog.Default(), + InstallDir: versionsDir, + LinkBinDir: filepath.Join(linkDir, "bin"), + CopyServiceFile: filepath.Join(linkDir, serviceDir, serviceName), + Log: slog.Default(), + TransformService: func(b []byte) []byte { + return []byte("[transform]" + string(b)) + }, } ctx := context.Background() @@ -821,7 +849,7 @@ func TestLocalInstaller_Unlink(t *testing.T) { {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("orig"), + svcCopy: []byte("[transform]orig"), }, { name: "different services", @@ -861,7 +889,7 @@ func TestLocalInstaller_Unlink(t *testing.T) { links: []symlink{ {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("orig"), + svcCopy: []byte("[transform]orig"), remaining: []string{servicePath}, }, { @@ -871,7 +899,7 @@ func TestLocalInstaller_Unlink(t *testing.T) { links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, }, - svcCopy: []byte("orig"), + svcCopy: []byte("[transform]orig"), }, { name: "wrong teleport link", @@ -881,7 +909,7 @@ func TestLocalInstaller_Unlink(t *testing.T) { {oldname: "other", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("orig"), + svcCopy: []byte("[transform]orig"), remaining: []string{servicePath, "bin/teleport"}, }, { @@ -892,7 +920,7 @@ func TestLocalInstaller_Unlink(t *testing.T) { {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "wrong", newname: "bin/tsh"}, }, - svcCopy: []byte("orig"), + svcCopy: []byte("[transform]orig"), remaining: []string{"bin/tsh"}, }, } @@ -944,10 +972,13 @@ func TestLocalInstaller_Unlink(t *testing.T) { } installer := &LocalInstaller{ - InstallDir: versionsDir, - LinkBinDir: filepath.Join(linkDir, "bin"), - LinkServiceDir: filepath.Join(linkDir, serviceDir), - Log: slog.Default(), + InstallDir: versionsDir, + LinkBinDir: filepath.Join(linkDir, "bin"), + CopyServiceFile: filepath.Join(linkDir, serviceDir, serviceName), + Log: slog.Default(), + TransformService: func(b []byte) []byte { + return []byte("[transform]" + string(b)) + }, } ctx := context.Background() err = installer.Unlink(ctx, version) diff --git a/lib/autoupdate/agent/setup.go b/lib/autoupdate/agent/setup.go index a8bf12a0afe0a..d180576c2a940 100644 --- a/lib/autoupdate/agent/setup.go +++ b/lib/autoupdate/agent/setup.go @@ -19,28 +19,45 @@ package agent import ( + "bytes" "context" "errors" "io/fs" "log/slog" "os" "path/filepath" + "regexp" "text/template" "github.com/google/renameio/v2" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/defaults" +) + +// Base paths for constructing namespaced directories. +const ( + teleportOptDir = "/opt/teleport" + versionsDirName = "versions" + systemdAdminDir = "/etc/systemd/system" + systemdPIDFile = "/run/teleport.pid" + defaultNamespace = "default" + systemNamespace = "system" + lockFileName = "update.lock" ) const ( updateServiceTemplate = `# teleport-update +# DO NOT EDIT THIS FILE [Unit] Description=Teleport auto-update service [Service] Type=oneshot -ExecStart={{.LinkDir}}/bin/teleport-update update +ExecStart={{.UpdaterCommand}} ` updateTimerTemplate = `# teleport-update +# DO NOT EDIT THIS FILE [Unit] Description=Teleport auto-update timer unit @@ -50,20 +67,124 @@ OnUnitActiveSec=5m RandomizedDelaySec=1m [Install] -WantedBy=teleport.service +WantedBy={{.TeleportService}} ` ) +// Namespace represents a namespace within various system paths for a isolated installation of Teleport. +type Namespace struct { + log *slog.Logger + // name of namespace + name string + // dataDir for Teleport + dataDir string + // linkDir for Teleport binaries (ns: /opt/teleport/myns/bin) + linkDir string + // versionsDir for Teleport versions (ns: /opt/teleport/myns/versions) + versionsDir string + // serviceFile for the Teleport systemd service (ns: /etc/systemd/system/teleport_myns.service) + serviceFile string + // configFile for Teleport config (ns: /opt/teleport/myns/etc/teleport.yaml) + configFile string + // pidFile for Teleport (ns: /run/teleport_myns.pid) + pidFile string + // updaterLockFile for locking the updater (ns: /opt/teleport/myns/update.lock) + updaterLockFile string + // updaterConfigFile for configuring updates (ns: /opt/teleport/myns/update.yaml) + updaterConfigFile string + // updaterBinFile for the updater when linked (linkDir + name) + updaterBinFile string + // updaterServiceFile is the systemd service path for the updater + updaterServiceFile string + // updaterTimerFile is the systemd timer path for the updater + updaterTimerFile string +} + +var alphanum = regexp.MustCompile("^[a-zA-Z0-9-]*$") + +// NewNamespace validates and returns a Namespace. +// Namespaces must be alphanumeric + `-`. +func NewNamespace(log *slog.Logger, name, dataDir, linkDir string) (*Namespace, error) { + if name == defaultNamespace || + name == systemNamespace { + return nil, trace.Errorf("namespace %q is reserved", name) + } + if !alphanum.MatchString(name) { + return nil, trace.Errorf("invalid namespace name %q, must be alphanumeric", name) + } + if name == "" { + if dataDir == "" { + dataDir = defaults.DataDir + } + if linkDir == "" { + linkDir = DefaultLinkDir + } + return &Namespace{ + log: log, + name: name, + dataDir: dataDir, + linkDir: linkDir, + versionsDir: filepath.Join(namespaceDir(name), versionsDirName), + serviceFile: filepath.Join("/", serviceDir, serviceName), + configFile: defaults.ConfigFilePath, + pidFile: systemdPIDFile, + updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), + updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), + updaterBinFile: filepath.Join(linkDir, BinaryName), + updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+".service"), + updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+".timer"), + }, nil + } + + prefix := "teleport_" + name + if dataDir == "" { + dataDir = filepath.Join(filepath.Dir(defaults.DataDir), prefix) + } + if linkDir == "" { + linkDir = filepath.Join(namespaceDir(name), "bin") + } + return &Namespace{ + log: log, + name: name, + dataDir: dataDir, + linkDir: linkDir, + versionsDir: filepath.Join(namespaceDir(name), versionsDirName), + serviceFile: filepath.Join(systemdAdminDir, prefix+".service"), + configFile: filepath.Join(filepath.Dir(defaults.ConfigFilePath), prefix+".yaml"), + pidFile: filepath.Join(filepath.Dir(systemdPIDFile), prefix+".pid"), + updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), + updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), + updaterBinFile: filepath.Join(linkDir, BinaryName), + updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".service"), + updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".timer"), + }, nil +} + +func namespaceDir(name string) string { + if name == "" { + name = defaultNamespace + } + return filepath.Join(teleportOptDir, name) +} + +// Init create the initial directory structure and returns the lockfile for a Namespace. +func (ns *Namespace) Init() (lockFile string, err error) { + if err := os.MkdirAll(ns.versionsDir, systemDirMode); err != nil { + return "", trace.Wrap(err) + } + return ns.updaterLockFile, nil +} + // Setup installs service and timer files for the teleport-update binary. // Afterwords, Setup reloads systemd and enables the timer with --now. -func Setup(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error { - err := writeConfigFiles(linkDir, dataDir) +func (ns *Namespace) Setup(ctx context.Context) error { + err := ns.writeConfigFiles() if err != nil { return trace.Errorf("failed to write teleport-update systemd config files: %w", err) } svc := &SystemdService{ - ServiceName: "teleport-update.timer", - Log: log, + ServiceName: filepath.Base(ns.updaterTimerFile), + Log: ns.log, } if err := svc.Sync(ctx); err != nil { return trace.Errorf("failed to sync systemd config: %w", err) @@ -75,46 +196,54 @@ func Setup(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error } // Teardown removes all traces of the auto-updater, including its configuration. -func Teardown(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error { +func (ns *Namespace) Teardown(ctx context.Context) error { svc := &SystemdService{ - ServiceName: "teleport-update.timer", - Log: log, + ServiceName: filepath.Base(ns.updaterTimerFile), + Log: ns.log, } if err := svc.Disable(ctx); err != nil { return trace.Errorf("failed to disable teleport-update systemd timer: %w", err) } - servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) - if err := os.Remove(servicePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := os.Remove(ns.updaterServiceFile); err != nil && !errors.Is(err, fs.ErrNotExist) { return trace.Errorf("failed to remove teleport-update systemd service: %w", err) } - timerPath := filepath.Join(linkDir, serviceDir, updateTimerName) - if err := os.Remove(timerPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := os.Remove(ns.updaterTimerFile); err != nil && !errors.Is(err, fs.ErrNotExist) { return trace.Errorf("failed to remove teleport-update systemd timer: %w", err) } if err := svc.Sync(ctx); err != nil { return trace.Errorf("failed to sync systemd config: %w", err) } - if err := os.RemoveAll(filepath.Join(dataDir, VersionsDirName)); err != nil { + if err := os.RemoveAll(ns.versionsDir); err != nil { return trace.Errorf("failed to remove versions directory: %w", err) } return nil } -func writeConfigFiles(linkDir, dataDir string) error { - servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) - err := writeTemplate(servicePath, updateServiceTemplate, linkDir, dataDir) +func (ns *Namespace) writeConfigFiles() error { + var args string + if ns.name != "" { + args = " --install-suffix=" + ns.name + } + err := writeTemplate( + ns.updaterServiceFile, updateServiceTemplate, + struct{ UpdaterCommand string }{ + ns.updaterBinFile + args + " update", + }, + ) if err != nil { return trace.Wrap(err) } - timerPath := filepath.Join(linkDir, serviceDir, updateTimerName) - err = writeTemplate(timerPath, updateTimerTemplate, linkDir, dataDir) + err = writeTemplate( + ns.updaterTimerFile, updateTimerTemplate, + struct{ TeleportService string }{filepath.Base(ns.serviceFile)}, + ) if err != nil { return trace.Wrap(err) } return nil } -func writeTemplate(path, t, linkDir, dataDir string) error { +func writeTemplate(path, t string, values any) error { dir, file := filepath.Split(path) if err := os.MkdirAll(dir, systemDirMode); err != nil { return trace.Wrap(err) @@ -133,12 +262,42 @@ func writeTemplate(path, t, linkDir, dataDir string) error { if err != nil { return trace.Wrap(err) } - err = tmpl.Execute(f, struct { - LinkDir string - DataDir string - }{linkDir, dataDir}) + err = tmpl.Execute(f, values) if err != nil { return trace.Wrap(err) } return trace.Wrap(f.CloseAtomicallyReplace()) } + +// replaceTeleportService replaces the default paths in the Teleport service config with namespaced paths. +func (ns *Namespace) replaceTeleportService(cfg []byte) []byte { + for _, rep := range []struct { + old, new string + }{ + { + old: "/usr/local/bin/", + new: ns.linkDir + "/", + }, + { + old: "/etc/teleport.yaml", + new: ns.configFile, + }, + { + old: "/run/teleport.pid", + new: ns.pidFile, + }, + } { + cfg = bytes.ReplaceAll(cfg, []byte(rep.old), []byte(rep.new)) + } + return cfg +} + +func (ns *Namespace) LogWarning(ctx context.Context) { + ns.log.WarnContext(ctx, "Custom install suffix specified. Teleport data_dir must be configured in the config file.", + "data_dir", ns.dataDir, + "path", ns.linkDir, + "config", ns.configFile, + "service", filepath.Base(ns.serviceFile), + "pid", ns.pidFile, + ) +} diff --git a/lib/autoupdate/agent/setup_test.go b/lib/autoupdate/agent/setup_test.go index 16cbdb5374fb6..8892acb85e06c 100644 --- a/lib/autoupdate/agent/setup_test.go +++ b/lib/autoupdate/agent/setup_test.go @@ -20,46 +20,172 @@ package agent import ( "bytes" + "log/slog" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" - libdefaults "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/utils/golden" ) -func TestWriteConfigFiles(t *testing.T) { - t.Parallel() - linkDir := t.TempDir() - dataDir := t.TempDir() - err := writeConfigFiles(linkDir, dataDir) - require.NoError(t, err) +func TestNewNamespace(t *testing.T) { + for _, p := range []struct { + name string + namespace string + linkDir string + dataDir string + errMatch string + ns *Namespace + }{ + { + name: "no namespace", + ns: &Namespace{ + dataDir: "/var/lib/teleport", + linkDir: "/usr/local/bin", + versionsDir: "/opt/teleport/default/versions", + serviceFile: "/lib/systemd/system/teleport.service", + configFile: "/etc/teleport.yaml", + pidFile: "/run/teleport.pid", + updaterLockFile: "/opt/teleport/default/update.lock", + updaterConfigFile: "/opt/teleport/default/update.yaml", + updaterBinFile: "/usr/local/bin/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update.service", + updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + }, + }, + { + name: "no namespace with dirs", + linkDir: "/link", + dataDir: "/data", + ns: &Namespace{ + dataDir: "/data", + linkDir: "/link", + versionsDir: "/opt/teleport/default/versions", + serviceFile: "/lib/systemd/system/teleport.service", + configFile: "/etc/teleport.yaml", + pidFile: "/run/teleport.pid", + updaterLockFile: "/opt/teleport/default/update.lock", + updaterConfigFile: "/opt/teleport/default/update.yaml", + updaterBinFile: "/link/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update.service", + updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + }, + }, + { + name: "test namespace", + namespace: "test", + ns: &Namespace{ + name: "test", + dataDir: "/var/lib/teleport_test", + linkDir: "/opt/teleport/test/bin", + versionsDir: "/opt/teleport/test/versions", + serviceFile: "/etc/systemd/system/teleport_test.service", + configFile: "/etc/teleport_test.yaml", + pidFile: "/run/teleport_test.pid", + updaterLockFile: "/opt/teleport/test/update.lock", + updaterConfigFile: "/opt/teleport/test/update.yaml", + updaterBinFile: "/opt/teleport/test/bin/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", + updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + }, + }, + { + name: "test namespace with dirs", + namespace: "test", + linkDir: "/link", + dataDir: "/data", + ns: &Namespace{ + name: "test", + dataDir: "/data", + linkDir: "/link", + versionsDir: "/opt/teleport/test/versions", + serviceFile: "/etc/systemd/system/teleport_test.service", + configFile: "/etc/teleport_test.yaml", + pidFile: "/run/teleport_test.pid", + updaterLockFile: "/opt/teleport/test/update.lock", + updaterConfigFile: "/opt/teleport/test/update.yaml", + updaterBinFile: "/link/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", + updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + }, + }, + { + name: "reserved default", + namespace: defaultNamespace, + errMatch: "reserved", + }, + { + name: "reserved system", + namespace: systemNamespace, + errMatch: "reserved", + }, + } { + t.Run(p.name, func(t *testing.T) { + log := slog.Default() + ns, err := NewNamespace(log, p.namespace, p.dataDir, p.linkDir) + if p.errMatch != "" { + require.Error(t, err) + require.Contains(t, err.Error(), p.errMatch) + return + } + require.NoError(t, err) + ns.log = nil + require.Equal(t, p.ns, ns) + }) + } +} - for _, p := range []string{ - filepath.Join(linkDir, serviceDir, updateServiceName), - filepath.Join(linkDir, serviceDir, updateTimerName), +func TestWriteConfigFiles(t *testing.T) { + for _, p := range []struct { + name string + namespace string + }{ + { + name: "no namespace", + }, + { + name: "test namespace", + namespace: "test", + }, } { - t.Run(filepath.Base(p), func(t *testing.T) { - data, err := os.ReadFile(p) + t.Run(p.name, func(t *testing.T) { + log := slog.Default() + linkDir := t.TempDir() + ns, err := NewNamespace(log, p.namespace, "", linkDir) + require.NoError(t, err) + ns.updaterServiceFile = filepath.Join(linkDir, serviceDir, filepath.Base(ns.updaterServiceFile)) + ns.updaterTimerFile = filepath.Join(linkDir, serviceDir, filepath.Base(ns.updaterTimerFile)) + err = ns.writeConfigFiles() + require.NoError(t, err) + + data, err := os.ReadFile(ns.updaterServiceFile) + require.NoError(t, err) + data = replaceValues(data, map[string]string{ + DefaultLinkDir: linkDir, + }) + if golden.ShouldSet() { + golden.SetNamed(t, "service", data) + } + require.Equal(t, string(golden.GetNamed(t, "service")), string(data)) + + data, err = os.ReadFile(ns.updaterTimerFile) require.NoError(t, err) data = replaceValues(data, map[string]string{ - DefaultLinkDir: linkDir, - libdefaults.DataDir: dataDir, + DefaultLinkDir: linkDir, }) if golden.ShouldSet() { - golden.Set(t, data) + golden.SetNamed(t, "timer", data) } - require.Equal(t, string(golden.Get(t)), string(data)) + require.Equal(t, string(golden.GetNamed(t, "timer")), string(data)) }) } } func replaceValues(data []byte, m map[string]string) []byte { for k, v := range m { - data = bytes.ReplaceAll(data, []byte(v), - []byte(k)) + data = bytes.ReplaceAll(data, []byte(v), []byte(k)) } return data } diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.service.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden similarity index 85% rename from lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.service.golden rename to lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden index 185b4f07a1aa9..6f9c2affce999 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.service.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden @@ -1,4 +1,5 @@ # teleport-update +# DO NOT EDIT THIS FILE [Unit] Description=Teleport auto-update service diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.timer.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden similarity index 87% rename from lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.timer.golden rename to lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden index acca095d9825f..d14a43d679e53 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/teleport-update.timer.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden @@ -1,4 +1,5 @@ # teleport-update +# DO NOT EDIT THIS FILE [Unit] Description=Teleport auto-update timer unit diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden new file mode 100644 index 0000000000000..030c99fd644e4 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden @@ -0,0 +1,8 @@ +# teleport-update +# DO NOT EDIT THIS FILE +[Unit] +Description=Teleport auto-update service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/teleport-update --install-suffix=test update diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden new file mode 100644 index 0000000000000..f57a3c08055bc --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden @@ -0,0 +1,12 @@ +# teleport-update +# DO NOT EDIT THIS FILE +[Unit] +Description=Teleport auto-update timer unit + +[Timer] +OnActiveSec=1m +OnUnitActiveSec=5m +RandomizedDelaySec=1m + +[Install] +WantedBy=teleport_test.service diff --git a/lib/autoupdate/agent/updater.go b/lib/autoupdate/agent/updater.go index 9c79a49d85967..283017b47fb2b 100644 --- a/lib/autoupdate/agent/updater.go +++ b/lib/autoupdate/agent/updater.go @@ -43,16 +43,14 @@ import ( const ( // DefaultLinkDir is the default location where Teleport is linked. - DefaultLinkDir = "/usr/local" - // DefaultSystemDir is the location where packaged Teleport binaries and services are installed. - DefaultSystemDir = "/usr/local/teleport-system" - // VersionsDirName specifies the name of the subdirectory inside the Teleport data dir for storing Teleport versions. - VersionsDirName = "versions" + DefaultLinkDir = "/usr/local/bin" // BinaryName specifies the name of the updater binary. BinaryName = "teleport-update" ) const ( + // defaultSystemDir is the location where packaged Teleport binaries and services are installed. + defaultSystemDir = "/opt/teleport/system" // cdnURITemplate is the default template for the Teleport tgz download. cdnURITemplate = "https://cdn.teleport.dev/teleport{{if .Enterprise}}-ent{{end}}-v{{.Version}}-{{.OS}}-{{.Arch}}{{if .FIPS}}-fips{{end}}-bin.tar.gz" // reservedFreeDisk is the minimum required free space left on disk during downloads. @@ -73,7 +71,7 @@ const ( // installations of the Teleport agent. // The AutoUpdater uses an HTTP client with sane defaults for downloads, and // will not fill disk to within 10 MB of available capacity. -func NewLocalUpdater(cfg LocalUpdaterConfig) (*Updater, error) { +func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) { certPool, err := x509.SystemCertPool() if err != nil { return nil, trace.Wrap(err) @@ -93,50 +91,40 @@ func NewLocalUpdater(cfg LocalUpdaterConfig) (*Updater, error) { if cfg.Log == nil { cfg.Log = slog.Default() } - if cfg.LinkDir == "" { - cfg.LinkDir = DefaultLinkDir - } if cfg.SystemDir == "" { - cfg.SystemDir = DefaultSystemDir - } - if cfg.DataDir == "" { - cfg.DataDir = libdefaults.DataDir - } - installDir := filepath.Join(cfg.DataDir, VersionsDirName) - if err := os.MkdirAll(installDir, systemDirMode); err != nil { - return nil, trace.Errorf("failed to create install directory: %w", err) + cfg.SystemDir = defaultSystemDir } return &Updater{ Log: cfg.Log, Pool: certPool, InsecureSkipVerify: cfg.InsecureSkipVerify, - ConfigPath: filepath.Join(installDir, updateConfigName), + ConfigPath: ns.updaterConfigFile, Installer: &LocalInstaller{ - InstallDir: installDir, - LinkBinDir: filepath.Join(cfg.LinkDir, "bin"), - // For backwards-compatibility with symlinks created by package-based installs, we always - // link into /lib/systemd/system, even though, e.g., /usr/local/lib/systemd/system would work. - LinkServiceDir: filepath.Join("/", serviceDir), + InstallDir: ns.versionsDir, + LinkBinDir: ns.linkDir, + CopyServiceFile: ns.serviceFile, SystemBinDir: filepath.Join(cfg.SystemDir, "bin"), - SystemServiceDir: filepath.Join(cfg.SystemDir, serviceDir), + SystemServiceFile: filepath.Join(cfg.SystemDir, serviceDir, serviceName), HTTP: client, Log: cfg.Log, ReservedFreeTmpDisk: reservedFreeDisk, ReservedFreeInstallDisk: reservedFreeDisk, + TransformService: ns.replaceTeleportService, }, Process: &SystemdService{ - ServiceName: "teleport.service", - PIDPath: "/run/teleport.pid", + ServiceName: filepath.Base(ns.serviceFile), + PIDPath: ns.pidFile, Log: cfg.Log, }, Setup: func(ctx context.Context) error { - name := filepath.Join(cfg.LinkDir, "bin", BinaryName) + name := ns.updaterBinFile if cfg.SelfSetup && runtime.GOOS == constants.LinuxOS { name = "/proc/self/exe" } cmd := exec.CommandContext(ctx, name, - "--data-dir", cfg.DataDir, - "--link-dir", cfg.LinkDir, + "--data-dir", ns.dataDir, + "--link-dir", ns.linkDir, + "--install-suffix", ns.name, "setup") cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -148,12 +136,8 @@ func NewLocalUpdater(cfg LocalUpdaterConfig) (*Updater, error) { } return trace.Wrap(err) }, - Revert: func(ctx context.Context) error { - return trace.Wrap(Setup(ctx, cfg.Log, cfg.LinkDir, cfg.DataDir)) - }, - Teardown: func(ctx context.Context) error { - return trace.Wrap(Teardown(ctx, cfg.Log, cfg.LinkDir, cfg.DataDir)) - }, + Revert: ns.Setup, + Teardown: ns.Teardown, }, nil } @@ -167,11 +151,7 @@ type LocalUpdaterConfig struct { // DownloadTimeout is a timeout for file download requests. // Defaults to no timeout. DownloadTimeout time.Duration - // DataDir for Teleport (usually /var/lib/teleport). - DataDir string - // LinkDir for installing Teleport (usually /usr/local). - LinkDir string - // SystemDir for package-installed Teleport installations (usually /usr/local/teleport-system). + // SystemDir for package-installed Teleport installations (usually /opt/teleport/system). SystemDir string // SelfSetup mode for using the current version of the teleport-update to setup the update service. SelfSetup bool diff --git a/lib/autoupdate/agent/updater_test.go b/lib/autoupdate/agent/updater_test.go index fb384cf737ca9..e4afd63740a76 100644 --- a/lib/autoupdate/agent/updater_test.go +++ b/lib/autoupdate/agent/updater_test.go @@ -84,12 +84,14 @@ func TestUpdater_Disable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case @@ -170,12 +172,14 @@ func TestUpdater_Unpin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case @@ -543,12 +547,14 @@ func TestUpdater_Update(t *testing.T) { t.Cleanup(server.Close) dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case @@ -759,12 +765,14 @@ func TestUpdater_LinkPackage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case @@ -969,12 +977,14 @@ func TestUpdater_Remove(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case @@ -1298,12 +1308,14 @@ func TestUpdater_Install(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + cfgPath := filepath.Join(dir, updateConfigName) + ns := &Namespace{ + updaterConfigFile: cfgPath, + } updater, err := NewLocalUpdater(LocalUpdaterConfig{ InsecureSkipVerify: true, - DataDir: dir, - }) + }, ns) require.NoError(t, err) // Create config file only if provided in test case diff --git a/lib/inventory/controller.go b/lib/inventory/controller.go index 86bd6450fb0c1..63873c03fc852 100644 --- a/lib/inventory/controller.go +++ b/lib/inventory/controller.go @@ -104,7 +104,7 @@ const ( instanceHeartbeatOk testEvent = "instance-heartbeat-ok" instanceHeartbeatErr testEvent = "instance-heartbeat-err" - timeReconciliationOk testEvent = "time-reconciliation-ok" + pongOk testEvent = "pong-ok" instanceCompareFailed testEvent = "instance-compare-failed" @@ -517,7 +517,6 @@ func (c *Controller) handleControlStream(handle *upstreamHandle) { handle.CloseWithError(err) return } - c.testEvent(timeReconciliationOk) case now := <-dbKeepAliveDelay.Elapsed(): dbKeepAliveDelay.Advance(now) @@ -631,6 +630,7 @@ func (c *Controller) handlePong(handle *upstreamHandle, msg proto.UpstreamInvent pending.rspC <- pong delete(handle.pings, msg.ID) + c.testEvent(pongOk) } func (c *Controller) handlePingRequest(handle *upstreamHandle, req pingRequest) error { diff --git a/lib/inventory/controller_test.go b/lib/inventory/controller_test.go index b89a8bdbae7dd..9ec509f725293 100644 --- a/lib/inventory/controller_test.go +++ b/lib/inventory/controller_test.go @@ -190,6 +190,27 @@ func TestSSHServerBasics(t *testing.T) { // set up fake in-memory control stream upstream, downstream := client.InventoryControlStreamPipe(client.ICSPipePeerAddr(peerAddr)) + t.Cleanup(func() { + controller.Close() + downstream.Close() + upstream.Close() + }) + + // launch goroutine to respond to ping requests + go func() { + for { + select { + case msg := <-downstream.Recv(): + downstream.Send(ctx, proto.UpstreamInventoryPong{ + ID: msg.(proto.DownstreamInventoryPing).ID, + }) + case <-downstream.Done(): + return + case <-ctx.Done(): + return + } + } + }() controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ ServerID: serverID, @@ -256,18 +277,6 @@ func TestSSHServerBasics(t *testing.T) { deny(sshKeepAliveErr, handlerClose), ) - // launch goroutine to respond to a single ping - go func() { - select { - case msg := <-downstream.Recv(): - downstream.Send(ctx, proto.UpstreamInventoryPong{ - ID: msg.(proto.DownstreamInventoryPing).ID, - }) - case <-downstream.Done(): - case <-ctx.Done(): - } - }() - // limit time of ping call pingCtx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() @@ -357,6 +366,27 @@ func TestAppServerBasics(t *testing.T) { // set up fake in-memory control stream upstream, downstream := client.InventoryControlStreamPipe() + t.Cleanup(func() { + controller.Close() + upstream.Close() + downstream.Close() + }) + + // launch goroutine to respond to ping requests + go func() { + for { + select { + case msg := <-downstream.Recv(): + downstream.Send(ctx, proto.UpstreamInventoryPong{ + ID: msg.(proto.DownstreamInventoryPing).ID, + }) + case <-downstream.Done(): + return + case <-ctx.Done(): + return + } + } + }() controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ ServerID: serverID, @@ -443,18 +473,6 @@ func TestAppServerBasics(t *testing.T) { deny(appKeepAliveErr, handlerClose), ) - // launch goroutine to respond to a single ping - go func() { - select { - case msg := <-downstream.Recv(): - downstream.Send(ctx, proto.UpstreamInventoryPong{ - ID: msg.(proto.DownstreamInventoryPing).ID, - }) - case <-downstream.Done(): - case <-ctx.Done(): - } - }() - // limit time of ping call pingCtx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() @@ -575,6 +593,27 @@ func TestDatabaseServerBasics(t *testing.T) { // set up fake in-memory control stream upstream, downstream := client.InventoryControlStreamPipe() + t.Cleanup(func() { + controller.Close() + upstream.Close() + downstream.Close() + }) + + // launch goroutine to respond to ping requests + go func() { + for { + select { + case msg := <-downstream.Recv(): + downstream.Send(ctx, proto.UpstreamInventoryPong{ + ID: msg.(proto.DownstreamInventoryPing).ID, + }) + case <-downstream.Done(): + return + case <-ctx.Done(): + return + } + } + }() controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ ServerID: serverID, @@ -662,18 +701,6 @@ func TestDatabaseServerBasics(t *testing.T) { deny(dbKeepAliveErr, handlerClose), ) - // launch goroutine to respond to a single ping - go func() { - select { - case msg := <-downstream.Recv(): - downstream.Send(ctx, proto.UpstreamInventoryPong{ - ID: msg.(proto.DownstreamInventoryPing).ID, - }) - case <-downstream.Done(): - case <-ctx.Done(): - } - }() - // limit time of ping call pingCtx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() @@ -1189,6 +1216,21 @@ func TestKubernetesServerBasics(t *testing.T) { // set up fake in-memory control stream upstream, downstream := client.InventoryControlStreamPipe() + // launch goroutine to respond to ping requests + go func() { + for { + select { + case msg := <-downstream.Recv(): + downstream.Send(ctx, proto.UpstreamInventoryPong{ + ID: msg.(proto.DownstreamInventoryPing).ID, + }) + case <-downstream.Done(): + return + case <-ctx.Done(): + return + } + } + }() controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ ServerID: serverID, @@ -1277,18 +1319,6 @@ func TestKubernetesServerBasics(t *testing.T) { deny(kubeKeepAliveErr, handlerClose), ) - // launch goroutine to respond to a single ping - go func() { - select { - case msg := <-downstream.Recv(): - downstream.Send(ctx, proto.UpstreamInventoryPong{ - ID: msg.(proto.DownstreamInventoryPing).ID, - }) - case <-downstream.Done(): - case <-ctx.Done(): - } - }() - // limit time of ping call pingCtx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() @@ -1473,12 +1503,6 @@ func TestTimeReconciliation(t *testing.T) { cancel() }) - controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ - ServerID: serverID, - Version: teleport.Version, - Services: []types.SystemRole{types.RoleNode}, - }) - // Launch goroutine to respond to clock request. go func() { for { @@ -1488,7 +1512,6 @@ func TestTimeReconciliation(t *testing.T) { ID: msg.(proto.DownstreamInventoryPing).ID, SystemClock: clock.Now().Add(-time.Minute).UTC(), }) - return case <-downstream.Done(): return case <-ctx.Done(): @@ -1497,12 +1520,16 @@ func TestTimeReconciliation(t *testing.T) { } }() + controller.RegisterControlStream(upstream, proto.UpstreamInventoryHello{ + ServerID: serverID, + Version: teleport.Version, + Services: []types.SystemRole{types.RoleNode}, + }) + _, ok := controller.GetControlStream(serverID) require.True(t, ok) - awaitEvents(t, events, - expect(timeReconciliationOk), - ) + awaitEvents(t, events, expect(pongOk)) awaitEvents(t, events, expect(instanceHeartbeatOk), deny(instanceHeartbeatErr, instanceCompareFailed, handlerClose), @@ -1510,6 +1537,8 @@ func TestTimeReconciliation(t *testing.T) { auth.mu.Lock() m := auth.lastInstance.GetLastMeasurement() auth.mu.Unlock() + + require.NotNil(t, m) require.InDelta(t, time.Minute, m.ControllerSystemClock.Sub(m.SystemClock)-m.RequestDuration/2, float64(time.Second)) } diff --git a/lib/inventory/inventory.go b/lib/inventory/inventory.go index 6f2f07df8533e..c96bcf4675ed9 100644 --- a/lib/inventory/inventory.go +++ b/lib/inventory/inventory.go @@ -404,9 +404,6 @@ type UpstreamHandle interface { Ping(ctx context.Context, id uint64) (d time.Duration, err error) - // SystemClock makes ping request to fetch the system clock of the node. - SystemClock(ctx context.Context, id uint64) (time.Time, time.Duration, error) - // HasService is a helper for checking if a given service is associated with this // stream. HasService(types.SystemRole) bool @@ -673,27 +670,6 @@ func (h *upstreamHandle) Ping(ctx context.Context, id uint64) (d time.Duration, } } -// SystemClock makes ping request to fetch the system clock of the downstream. -func (h *upstreamHandle) SystemClock(ctx context.Context, id uint64) (time.Time, time.Duration, error) { - rspC := make(chan pingResponse, 1) - select { - case h.pingC <- pingRequest{rspC: rspC, id: id}: - case <-h.Done(): - return time.Time{}, 0, trace.Errorf("failed to send downstream ping (stream closed)") - case <-ctx.Done(): - return time.Time{}, 0, trace.Errorf("failed to send downstream ping: %v", ctx.Err()) - } - - select { - case rsp := <-rspC: - return rsp.systemClock, rsp.reqDuration, rsp.err - case <-h.Done(): - return time.Time{}, 0, trace.Errorf("failed to recv upstream pong (stream closed)") - case <-ctx.Done(): - return time.Time{}, 0, trace.Errorf("failed to recv upstream ping: %v", ctx.Err()) - } -} - func (h *upstreamHandle) Hello() proto.UpstreamInventoryHello { return h.hello } diff --git a/lib/teleterm/teleterm_test.go b/lib/teleterm/teleterm_test.go index 854273d71c683..bf7b2f6a2e548 100644 --- a/lib/teleterm/teleterm_test.go +++ b/lib/teleterm/teleterm_test.go @@ -27,6 +27,7 @@ import ( "net" "os" "path/filepath" + "slices" "testing" "time" @@ -226,5 +227,13 @@ func createValidClientTLSConfig(t *testing.T, certsDir string) *tls.Config { tlsConfig, err := createClientTLSConfig(clientCert, serverCertPath) require.NoError(t, err) + // this would be done by the grpc TransportCredential in the grpc client, + // but we're going to fake it with just a tls.Client, so we have to add the + // http2 next proto ourselves (enforced by grpc-go starting from v1.67, and + // required by the http2 spec when speaking http2 in TLS) + if !slices.Contains(tlsConfig.NextProtos, "h2") { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } + return tlsConfig } diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index e6702ae9f35c2..5d34457b8882b 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -25,7 +25,6 @@ import ( "log/slog" "os" "os/signal" - "path/filepath" "syscall" "github.com/gravitational/trace" @@ -41,8 +40,7 @@ import ( const appHelp = `Teleport Updater -The Teleport Updater updates the version a Teleport agent on a Linux server -that is being used as agent to provide connectivity to Teleport resources. +The Teleport Updater automatically updates a Teleport agent. The Teleport Updater supports upgrade schedules and automated rollbacks. @@ -59,22 +57,16 @@ const ( updateVersionEnvVar = "TELEPORT_UPDATE_VERSION" ) -const ( - // lockFileName specifies the name of the file containing the flock lock preventing concurrent updater execution. - lockFileName = ".update-lock" -) - var plog = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentUpdater) func main() { - if err := Run(os.Args[1:]); err != nil { - libutils.FatalError(err) + if code := Run(os.Args[1:]); code != 0 { + os.Exit(code) } } type cliConfig struct { autoupdate.OverrideConfig - // Debug logs enabled Debug bool // LogFormat controls the format of logging. Can be either `json` or `text`. @@ -84,12 +76,18 @@ type cliConfig struct { DataDir string // LinkDir for linking binaries and systemd services LinkDir string + // InstallSuffix is the isolated suffix for the installation. + InstallSuffix string // SelfSetup mode for using the current version of the teleport-update to setup the update service. SelfSetup bool } -func Run(args []string) error { - var ccfg cliConfig +func Run(args []string) int { + var ( + ccfg cliConfig + userLinkDir bool + userDataDir bool + ) ctx := context.Background() ctx, _ = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) @@ -97,45 +95,47 @@ func Run(args []string) error { app.Flag("debug", "Verbose logging to stdout."). Short('d').BoolVar(&ccfg.Debug) app.Flag("data-dir", "Teleport data directory. Access to this directory should be limited."). - Default(libdefaults.DataDir).StringVar(&ccfg.DataDir) + Default(libdefaults.DataDir).IsSetByUser(&userDataDir).StringVar(&ccfg.DataDir) app.Flag("log-format", "Controls the format of output logs. Can be `json` or `text`. Defaults to `text`."). Default(libutils.LogFormatText).EnumVar(&ccfg.LogFormat, libutils.LogFormatJSON, libutils.LogFormatText) - app.Flag("link-dir", "Directory to link the active Teleport installation into."). - Default(autoupdate.DefaultLinkDir).Hidden().StringVar(&ccfg.LinkDir) + app.Flag("install-suffix", "Suffix for creating an agent installation outside of the default $PATH. Note: this changes the default data directory."). + Short('n').StringVar(&ccfg.InstallSuffix) + app.Flag("link-dir", "Directory to link the active Teleport installation's binaries into."). + Default(autoupdate.DefaultLinkDir).IsSetByUser(&userLinkDir).Hidden().StringVar(&ccfg.LinkDir) app.HelpFlag.Short('h') versionCmd := app.Command("version", fmt.Sprintf("Print the version of your %s binary.", autoupdate.BinaryName)) - enableCmd := app.Command("enable", "Enable agent auto-updates and perform initial installation or update.") + enableCmd := app.Command("enable", "Enable agent auto-updates and perform initial installation or update. This creates a systemd timer that periodically runs the update subcommand.") enableCmd.Flag("proxy", "Address of the Teleport Proxy."). Short('p').Envar(proxyServerEnvVar).StringVar(&ccfg.Proxy) enableCmd.Flag("group", "Update group for this agent installation."). Short('g').Envar(updateGroupEnvVar).StringVar(&ccfg.Group) - enableCmd.Flag("template", "Go template used to override Teleport download URL."). + enableCmd.Flag("template", "Go template used to override the Teleport download URL."). Short('t').Envar(templateEnvVar).StringVar(&ccfg.URLTemplate) - enableCmd.Flag("force-version", "Force the provided version instead of querying it from the Teleport cluster."). + enableCmd.Flag("force-version", "Force the provided version instead of using the version provided by the Teleport cluster."). Short('f').Envar(updateVersionEnvVar).Hidden().StringVar(&ccfg.ForceVersion) enableCmd.Flag("self-setup", "Use the current teleport-update binary to create systemd service config for auto-updates."). Short('s').Hidden().BoolVar(&ccfg.SelfSetup) // TODO(sclevine): add force-fips and force-enterprise as hidden flags - pinCmd := app.Command("pin", "Install Teleport and lock the updater on the installed version.") + pinCmd := app.Command("pin", "Install Teleport and lock the updater to the installed version.") pinCmd.Flag("proxy", "Address of the Teleport Proxy."). Short('p').Envar(proxyServerEnvVar).StringVar(&ccfg.Proxy) pinCmd.Flag("group", "Update group for this agent installation."). Short('g').Envar(updateGroupEnvVar).StringVar(&ccfg.Group) pinCmd.Flag("template", "Go template used to override Teleport download URL."). Short('t').Envar(templateEnvVar).StringVar(&ccfg.URLTemplate) - pinCmd.Flag("force-version", "Force the provided version instead of querying it from the Teleport cluster."). + pinCmd.Flag("force-version", "Force the provided version instead of using the version provided by the Teleport cluster."). Short('f').Envar(updateVersionEnvVar).StringVar(&ccfg.ForceVersion) pinCmd.Flag("self-setup", "Use the current teleport-update binary to create systemd service config for auto-updates."). Short('s').Hidden().BoolVar(&ccfg.SelfSetup) - disableCmd := app.Command("disable", "Disable agent auto-updates.") + disableCmd := app.Command("disable", "Disable agent auto-updates. Does not affect the active installation of Teleport.") unpinCmd := app.Command("unpin", "Unpin the current version, allowing it to be updated.") - updateCmd := app.Command("update", "Update agent to the latest version, if a new version is available.") + updateCmd := app.Command("update", "Update the agent to the latest version, if a new version is available.") updateCmd.Flag("self-setup", "Use the current teleport-update binary to create systemd service config for auto-updates."). Short('s').Hidden().BoolVar(&ccfg.SelfSetup) @@ -153,12 +153,23 @@ func Run(args []string) error { command, err := app.Parse(args) if err != nil { app.Usage(args) - return trace.Wrap(err) + libutils.FatalError(err) } + + // These have different defaults if --install-suffix is specified. + // If the user did not set them, let autoupdate.NewNamespace set them. + if !userDataDir { + ccfg.DataDir = "" + } + if !userLinkDir { + ccfg.LinkDir = "" + } + // Logging must be configured as early as possible to ensure all log // message are formatted correctly. if err := setupLogger(ccfg.Debug, ccfg.LogFormat); err != nil { - return trace.Errorf("failed to set up logger") + plog.ErrorContext(ctx, "Failed to set up logger.", "error", err) + return 1 } switch command { @@ -175,9 +186,9 @@ func Run(args []string) error { case updateCmd.FullCommand(): err = cmdUpdate(ctx, &ccfg) case linkCmd.FullCommand(): - err = cmdLink(ctx, &ccfg) + err = cmdLinkPackage(ctx, &ccfg) case unlinkCmd.FullCommand(): - err = cmdUnlink(ctx, &ccfg) + err = cmdUnlinkPackage(ctx, &ccfg) case setupCmd.FullCommand(): err = cmdSetup(ctx, &ccfg) case statusCmd.FullCommand(): @@ -190,8 +201,14 @@ func Run(args []string) error { // This should only happen when there's a missing switch case above. err = trace.Errorf("command %q not configured", command) } - - return err + if errors.Is(err, autoupdate.ErrNotSupported) { + return autoupdate.CodeNotSupported + } + if err != nil { + plog.ErrorContext(ctx, "Command failed.", "error", err) + return 1 + } + return 0 } func setupLogger(debug bool, format string) error { @@ -211,19 +228,41 @@ func setupLogger(debug bool, format string) error { return nil } -// cmdDisable disables updates. -func cmdDisable(ctx context.Context, ccfg *cliConfig) error { +func initConfig(ccfg *cliConfig) (updater *autoupdate.Updater, lockFile string, err error) { + ns, err := autoupdate.NewNamespace(plog, ccfg.InstallSuffix, ccfg.DataDir, ccfg.LinkDir) + if err != nil { + return nil, "", trace.Wrap(err) + } + lockFile, err = ns.Init() + if err != nil { + return nil, "", trace.Wrap(err) + } + updater, err = autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ + SelfSetup: ccfg.SelfSetup, + Log: plog, + }, ns) + return updater, lockFile, trace.Wrap(err) +} + +func statusConfig(ccfg *cliConfig) (*autoupdate.Updater, error) { + ns, err := autoupdate.NewNamespace(plog, ccfg.InstallSuffix, ccfg.DataDir, ccfg.LinkDir) + if err != nil { + return nil, trace.Wrap(err) + } updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, SelfSetup: ccfg.SelfSetup, Log: plog, - }) + }, ns) + return updater, trace.Wrap(err) +} + +// cmdDisable disables updates. +func cmdDisable(ctx context.Context, ccfg *cliConfig) error { + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } - unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSWriteLock(lockFile) if err != nil { return trace.Errorf("failed to grab concurrent execution lock: %w", err) } @@ -240,17 +279,11 @@ func cmdDisable(ctx context.Context, ccfg *cliConfig) error { // cmdUnpin unpins the current version. func cmdUnpin(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to setup updater: %w", err) } - unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSWriteLock(lockFile) if err != nil { return trace.Errorf("failed to grab concurrent execution lock: %w", err) } @@ -267,19 +300,20 @@ func cmdUnpin(ctx context.Context, ccfg *cliConfig) error { // cmdInstall installs Teleport and sets configuration. func cmdInstall(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) + if ccfg.InstallSuffix != "" { + ns, err := autoupdate.NewNamespace(plog, ccfg.InstallSuffix, ccfg.DataDir, ccfg.LinkDir) + if err != nil { + return trace.Wrap(err) + } + ns.LogWarning(ctx) + } + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } // Ensure enable can't run concurrently. - unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSWriteLock(lockFile) if err != nil { return trace.Errorf("failed to grab concurrent execution lock: %w", err) } @@ -296,18 +330,12 @@ func cmdInstall(ctx context.Context, ccfg *cliConfig) error { // cmdUpdate updates Teleport to the version specified by cluster reachable at the proxy address. func cmdUpdate(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } // Ensure update can't run concurrently. - unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSWriteLock(lockFile) if err != nil { return trace.Errorf("failed to grab concurrent execution lock: %w", err) } @@ -323,21 +351,15 @@ func cmdUpdate(ctx context.Context, ccfg *cliConfig) error { return nil } -// cmdLink creates system package links if no version is linked and auto-updates is disabled. -func cmdLink(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) +// cmdLinkPackage creates system package links if no version is linked and auto-updates is disabled. +func cmdLinkPackage(ctx context.Context, ccfg *cliConfig) error { + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } // Skip operation and warn if the updater is currently running. - unlock, err := libutils.FSTryReadLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSTryReadLock(lockFile) if errors.Is(err, libutils.ErrUnsuccessfulLockTry) { plog.WarnContext(ctx, "Updater is currently running. Skipping package linking.") return nil @@ -357,21 +379,15 @@ func cmdLink(ctx context.Context, ccfg *cliConfig) error { return nil } -// cmdUnlink remove system package links. -func cmdUnlink(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) +// cmdUnlinkPackage remove system package links. +func cmdUnlinkPackage(ctx context.Context, ccfg *cliConfig) error { + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to setup updater: %w", err) } // Error if the updater is running. We could remove its links by accident. - unlock, err := libutils.FSTryWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSTryWriteLock(lockFile) if errors.Is(err, libutils.ErrUnsuccessfulLockTry) { return trace.Errorf("updater is currently running") } @@ -392,10 +408,14 @@ func cmdUnlink(ctx context.Context, ccfg *cliConfig) error { // cmdSetup writes configuration files that are needed to run teleport-update update. func cmdSetup(ctx context.Context, ccfg *cliConfig) error { - err := autoupdate.Setup(ctx, plog, ccfg.LinkDir, ccfg.DataDir) + ns, err := autoupdate.NewNamespace(plog, ccfg.InstallSuffix, ccfg.DataDir, ccfg.LinkDir) + if err != nil { + return trace.Wrap(err) + } + err = ns.Setup(ctx) if errors.Is(err, autoupdate.ErrNotSupported) { plog.WarnContext(ctx, "Not enabling systemd service because systemd is not running.") - os.Exit(autoupdate.CodeNotSupported) + return trace.Wrap(err) } if err != nil { return trace.Errorf("failed to setup teleport-update service: %w", err) @@ -405,13 +425,7 @@ func cmdSetup(ctx context.Context, ccfg *cliConfig) error { // cmdStatus displays auto-update status. func cmdStatus(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) + updater, err := statusConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } @@ -425,18 +439,12 @@ func cmdStatus(ctx context.Context, ccfg *cliConfig) error { // cmdUninstall removes the updater-managed install of Teleport and gracefully reverts back to the Teleport package. func cmdUninstall(ctx context.Context, ccfg *cliConfig) error { - updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ - DataDir: ccfg.DataDir, - LinkDir: ccfg.LinkDir, - SystemDir: autoupdate.DefaultSystemDir, - SelfSetup: ccfg.SelfSetup, - Log: plog, - }) + updater, lockFile, err := initConfig(ccfg) if err != nil { return trace.Errorf("failed to initialize updater: %w", err) } // Ensure update can't run concurrently. - unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + unlock, err := libutils.FSWriteLock(lockFile) if err != nil { return trace.Errorf("failed to grab concurrent execution lock: %w", err) } diff --git a/web/packages/shared/components/MissingPermissionsTooltip/MissingPermissionsTooltip.tsx b/web/packages/shared/components/MissingPermissionsTooltip/MissingPermissionsTooltip.tsx new file mode 100644 index 0000000000000..aaef46ec0baf6 --- /dev/null +++ b/web/packages/shared/components/MissingPermissionsTooltip/MissingPermissionsTooltip.tsx @@ -0,0 +1,39 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Box, Text, Flex } from 'design'; + +export const MissingPermissionsTooltip = ({ + missingPermissions, +}: { + missingPermissions: string[]; +}) => { + return ( + + You do not have all of the required permissions. + + Missing permissions: + + {missingPermissions.map(perm => ( + {perm} + ))} + + + + ); +}; diff --git a/web/packages/shared/components/MissingPermissionsTooltip/index.ts b/web/packages/shared/components/MissingPermissionsTooltip/index.ts new file mode 100644 index 0000000000000..26c2679b0e46f --- /dev/null +++ b/web/packages/shared/components/MissingPermissionsTooltip/index.ts @@ -0,0 +1,19 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export { MissingPermissionsTooltip } from './MissingPermissionsTooltip'; diff --git a/web/packages/teleport/src/Roles/RoleList/RoleList.tsx b/web/packages/teleport/src/Roles/RoleList/RoleList.tsx index d537eba3a43ad..5641442e43d78 100644 --- a/web/packages/teleport/src/Roles/RoleList/RoleList.tsx +++ b/web/packages/teleport/src/Roles/RoleList/RoleList.tsx @@ -24,6 +24,7 @@ import { SearchPanel } from 'shared/components/Search'; import { SeversidePagination } from 'teleport/components/hooks/useServersidePagination'; import { RoleResource } from 'teleport/services/resources'; +import { Access } from 'teleport/services/user'; export function RoleList({ onEdit, @@ -31,13 +32,18 @@ export function RoleList({ onSearchChange, search, serversidePagination, + rolesAcl, }: { onEdit(id: string): void; onDelete(id: string): void; onSearchChange(search: string): void; search: string; serversidePagination: SeversidePagination; + rolesAcl: Access; }) { + const canEdit = rolesAcl.edit; + const canDelete = rolesAcl.remove; + return ( ( onEdit(role.id)} onDelete={() => onDelete(role.id)} /> @@ -80,12 +88,22 @@ export function RoleList({ ); } -const ActionCell = (props: { onEdit(): void; onDelete(): void }) => { +const ActionCell = (props: { + canEdit: boolean; + canDelete: boolean; + onEdit(): void; + onDelete(): void; +}) => { + if (!(props.canEdit || props.canDelete)) { + return ; + } return ( - Edit... - Delete... + {props.canEdit && Edit} + {props.canDelete && ( + Delete + )} ); diff --git a/web/packages/teleport/src/Roles/Roles.story.tsx b/web/packages/teleport/src/Roles/Roles.story.tsx index c0d197a8f0196..f5be3186c0eaf 100644 --- a/web/packages/teleport/src/Roles/Roles.story.tsx +++ b/web/packages/teleport/src/Roles/Roles.story.tsx @@ -81,4 +81,11 @@ const sample = { remove: () => null, create: () => null, update: () => null, + rolesAcl: { + list: true, + create: true, + remove: true, + edit: true, + read: true, + }, }; diff --git a/web/packages/teleport/src/Roles/Roles.test.tsx b/web/packages/teleport/src/Roles/Roles.test.tsx new file mode 100644 index 0000000000000..1edae5ee235e6 --- /dev/null +++ b/web/packages/teleport/src/Roles/Roles.test.tsx @@ -0,0 +1,211 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { MemoryRouter } from 'react-router'; +import { render, screen, fireEvent, waitFor } from 'design/utils/testing'; + +import { ContextProvider } from 'teleport'; +import { createTeleportContext } from 'teleport/mocks/contexts'; + +import { Roles } from './Roles'; +import { State } from './useRoles'; + +describe('Roles list', () => { + const defaultState: State = { + create: jest.fn(), + fetch: jest.fn(), + remove: jest.fn(), + update: jest.fn(), + rolesAcl: { + read: true, + remove: true, + create: true, + edit: true, + list: true, + }, + }; + + beforeEach(() => { + jest.spyOn(defaultState, 'fetch').mockResolvedValue({ + startKey: '', + items: [ + { + content: '', + id: '1', + kind: 'role', + name: 'cool-role', + description: 'coolest-role', + }, + ], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('button is enabled if user has create perms', async () => { + const ctx = createTeleportContext(); + render( + + + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('create_new_role_button')).toBeEnabled(); + }); + }); + + test('displays disabled create button', async () => { + const ctx = createTeleportContext(); + const testState = { + ...defaultState, + rolesAcl: { + ...defaultState.rolesAcl, + create: false, + }, + }; + + render( + + + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('create_new_role_button')).toBeDisabled(); + }); + }); + + test('all options available', async () => { + const ctx = createTeleportContext(); + + render( + + + + + + ); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /options/i }) + ).toBeInTheDocument(); + }); + const optionsButton = screen.getByRole('button', { name: /options/i }); + fireEvent.click(optionsButton); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems).toHaveLength(2); + }); + + test('hides edit button if no access', async () => { + const ctx = createTeleportContext(); + const testState = { + ...defaultState, + rolesAcl: { + ...defaultState.rolesAcl, + edit: false, + }, + }; + + render( + + + + + + ); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /options/i }) + ).toBeInTheDocument(); + }); + const optionsButton = screen.getByRole('button', { name: /options/i }); + fireEvent.click(optionsButton); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems).toHaveLength(1); + expect(menuItems.every(item => item.textContent.includes('Edit'))).not.toBe( + true + ); + }); + + test('hides delete button if no access', async () => { + const ctx = createTeleportContext(); + const testState = { + ...defaultState, + rolesAcl: { + ...defaultState.rolesAcl, + remove: false, + }, + }; + + render( + + + + + + ); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /options/i }) + ).toBeInTheDocument(); + }); + const optionsButton = screen.getByRole('button', { name: /options/i }); + fireEvent.click(optionsButton); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems).toHaveLength(1); + expect( + menuItems.every(item => item.textContent.includes('Delete')) + ).not.toBe(true); + }); + + test('hides Options button if no permissions to edit or delete', async () => { + const ctx = createTeleportContext(); + const testState = { + ...defaultState, + rolesAcl: { + ...defaultState.rolesAcl, + remove: false, + edit: false, + }, + }; + + render( + + + + + + ); + + await waitFor(() => { + expect(screen.getByText('cool-role')).toBeInTheDocument(); + }); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems).toHaveLength(0); + }); +}); diff --git a/web/packages/teleport/src/Roles/Roles.tsx b/web/packages/teleport/src/Roles/Roles.tsx index d04034b475609..3bf968247b6ba 100644 --- a/web/packages/teleport/src/Roles/Roles.tsx +++ b/web/packages/teleport/src/Roles/Roles.tsx @@ -22,6 +22,8 @@ import { P } from 'design/Text/Text'; import { useAsync } from 'shared/hooks/useAsync'; import { Danger } from 'design/Alert'; import { useTheme } from 'styled-components'; +import { MissingPermissionsTooltip } from 'shared/components/MissingPermissionsTooltip'; +import { HoverTooltip } from 'shared/components/ToolTip'; import { FeatureBox, @@ -55,7 +57,7 @@ export function RolesContainer() { const useNewRoleEditor = storageService.getUseNewRoleEditor(); export function Roles(props: State) { - const { remove, create, update, fetch } = props; + const { remove, create, update, fetch, rolesAcl } = props; const [search, setSearch] = useState(''); const serverSidePagination = useServerSidePagination({ @@ -142,24 +144,41 @@ export function Roles(props: State) { } } + const canCreate = rolesAcl.create; + return ( - + Roles - + + {serverSidePagination.attempt.status === 'failed' && ( @@ -172,6 +191,7 @@ export function Roles(props: State) { search={search} onEdit={handleEdit} onDelete={resources.remove} + rolesAcl={rolesAcl} /> diff --git a/web/packages/teleport/src/Roles/useRoles.ts b/web/packages/teleport/src/Roles/useRoles.ts index 9a926a9c28f59..6c4e9cc5f0f47 100644 --- a/web/packages/teleport/src/Roles/useRoles.ts +++ b/web/packages/teleport/src/Roles/useRoles.ts @@ -24,6 +24,8 @@ import { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; import type { UrlListRolesParams } from 'teleport/config'; export function useRoles(ctx: TeleportContext) { + const rolesAcl = ctx.storeUser.getRoleAccess(); + async function create(role: Partial) { return ctx.resourceService.createRole(await toYaml(role)); } @@ -45,6 +47,7 @@ export function useRoles(ctx: TeleportContext) { create, update, remove, + rolesAcl, }; }