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 (
-
-
+ {props.canEdit && }
+ {props.canDelete && (
+
+ )}
|
);
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
-
+ ) : (
+ ''
+ )
}
- ml="auto"
- width="240px"
- onClick={handleCreate}
>
- Create New Role
-
+
+
{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,
};
}