Skip to content

Commit cfa8df9

Browse files
committed
[backend] kubernetes: fix secret size limitation
By now kubernetes backend could hold up to defaultETCDSize gzipped data (which is 1-1.5Mb). This doesn't scale for larger states. This commit implements spliting data across multiple secrets bound by the same Secret labels. This practically removes etcd value size limitation and allows backend to scale across multiple secrets. This also takes care of cases when state needs to be shrinked. In such case code will cleanup unneeded secrets Signed-off-by: Dinar Valeev <dinar.valeev@absa.africa>
1 parent a742d7e commit cfa8df9

File tree

2 files changed

+107
-44
lines changed

2 files changed

+107
-44
lines changed

internal/backend/remote-state/kubernetes/backend_state.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
9494
return nil, err
9595
}
9696

97-
secretName, err := c.createSecretName()
97+
// get base secret name
98+
secretName, err := c.createSecretName(0)
9899
if err != nil {
99100
return nil, err
100101
}

internal/backend/remote-state/kubernetes/client.go

Lines changed: 105 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
tfstateWorkspaceKey = "tfstateWorkspace"
3131
tfstateLockInfoAnnotation = "app.terraform.io/lock-info"
3232
managedByKey = "app.kubernetes.io/managed-by"
33+
etcdDefaultSize = 1048576
3334
)
3435

3536
type RemoteClient struct {
@@ -42,28 +43,27 @@ type RemoteClient struct {
4243
}
4344

4445
func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
45-
secretName, err := c.createSecretName()
46+
secretList, err := c.getSecrets()
4647
if err != nil {
4748
return nil, err
4849
}
49-
secret, err := c.kubernetesSecretClient.Get(secretName, metav1.GetOptions{})
50-
if err != nil {
51-
if k8serrors.IsNotFound(err) {
52-
return nil, nil
53-
}
54-
return nil, err
55-
}
5650

57-
secretData := getSecretData(secret)
58-
stateRaw, ok := secretData[tfstateKey]
59-
if !ok {
60-
// The secret exists but there is no state in it
51+
if len(secretList.Items) == 0 {
6152
return nil, nil
6253
}
6354

64-
stateRawString := stateRaw.(string)
55+
var data []string
56+
for _, secret := range secretList.Items {
57+
secretData := getSecretData(&secret)
58+
stateRaw, ok := secretData[tfstateKey]
59+
if !ok {
60+
// The secret exists but there is no state in it
61+
return nil, nil
62+
}
63+
data = append(data, stateRaw.(string))
64+
}
6565

66-
state, err := uncompressState(stateRawString)
66+
state, err := uncompressState(data)
6767
if err != nil {
6868
return nil, err
6969
}
@@ -77,8 +77,18 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
7777
return p, nil
7878
}
7979

80+
func multiSecret(size []byte) bool {
81+
// by default etcd can hold up to 1.5Mib data for secret
82+
return len(size) > etcdDefaultSize
83+
}
84+
85+
func (c *RemoteClient) getSecrets() (*unstructured.UnstructuredList, error) {
86+
ls := metav1.SetAsLabelSelector(c.getLabels())
87+
return c.kubernetesSecretClient.List(metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(ls)})
88+
}
89+
8090
func (c *RemoteClient) Put(data []byte) error {
81-
secretName, err := c.createSecretName()
91+
secretName, err := c.createSecretName(0)
8292
if err != nil {
8393
return err
8494
}
@@ -88,46 +98,90 @@ func (c *RemoteClient) Put(data []byte) error {
8898
return err
8999
}
90100

91-
secret, err := c.getSecret(secretName)
101+
parts := split(payload, etcdDefaultSize)
102+
existingSecrets, err := c.getSecrets()
92103
if err != nil {
93-
if !k8serrors.IsNotFound(err) {
94-
return err
95-
}
104+
return err
105+
}
96106

97-
secret = &unstructured.Unstructured{
98-
Object: map[string]interface{}{
99-
"metadata": metav1.ObjectMeta{
100-
Name: secretName,
101-
Namespace: c.namespace,
102-
Labels: c.getLabels(),
103-
Annotations: map[string]string{"encoding": "gzip"},
104-
},
105-
},
107+
for idx, data := range parts {
108+
secretName, err := c.createSecretName(idx)
109+
if err != nil {
110+
return err
106111
}
107112

108-
secret, err = c.kubernetesSecretClient.Create(secret, metav1.CreateOptions{})
113+
secret, err := c.getSecret(secretName)
109114
if err != nil {
110-
return err
115+
if !k8serrors.IsNotFound(err) {
116+
return err
117+
}
118+
119+
secret = c.formatSecret(secretName)
120+
121+
secret, err = c.kubernetesSecretClient.Create(secret, metav1.CreateOptions{})
122+
if err != nil {
123+
return err
124+
}
111125
}
126+
127+
setState(secret, data)
128+
_, err = c.kubernetesSecretClient.Update(secret, metav1.UpdateOptions{})
112129
}
113130

114-
setState(secret, payload)
115-
_, err = c.kubernetesSecretClient.Update(secret, metav1.UpdateOptions{})
131+
// in case new state requires less secrets, cleanup old secrets
132+
secretNum := len(existingSecrets.Items)
133+
newSecretNum := len(parts)
134+
for i := newSecretNum; i <= secretNum; i++ {
135+
c.deleteSecret(fmt.Sprintf("%s-part%d", secretName, i))
136+
}
116137
return err
117138
}
118139

140+
func (c *RemoteClient) formatSecret(name string) *unstructured.Unstructured {
141+
return &unstructured.Unstructured{
142+
Object: map[string]interface{}{
143+
"metadata": metav1.ObjectMeta{
144+
Name: name,
145+
Namespace: c.namespace,
146+
Labels: c.getLabels(),
147+
Annotations: map[string]string{"encoding": "gzip"},
148+
},
149+
},
150+
}
151+
}
152+
153+
func split(buf []byte, size int) [][]byte {
154+
var chunk []byte
155+
chunks := make([][]byte, 0, len(buf)/size+1)
156+
for len(buf) >= size {
157+
chunk, buf = buf[:size], buf[size:]
158+
chunks = append(chunks, chunk)
159+
}
160+
if len(buf) > 0 {
161+
chunks = append(chunks, buf[:len(buf)])
162+
}
163+
return chunks
164+
}
165+
119166
// Delete the state secret
120167
func (c *RemoteClient) Delete() error {
121-
secretName, err := c.createSecretName()
168+
secretList, err := c.getSecrets()
122169
if err != nil {
123170
return err
124171
}
125172

126-
err = c.deleteSecret(secretName)
127-
if err != nil {
128-
if !k8serrors.IsNotFound(err) {
173+
for i, _ := range secretList.Items {
174+
secretName, err := c.createSecretName(i)
175+
if err != nil {
129176
return err
130177
}
178+
179+
err = c.deleteSecret(secretName)
180+
if err != nil {
181+
if !k8serrors.IsNotFound(err) {
182+
return err
183+
}
184+
}
131185
}
132186

133187
leaseName, err := c.createLeaseName()
@@ -317,9 +371,13 @@ func (c *RemoteClient) deleteLease(name string) error {
317371
return c.kubernetesLeaseClient.Delete(name, delOps)
318372
}
319373

320-
func (c *RemoteClient) createSecretName() (string, error) {
374+
func (c *RemoteClient) createSecretName(idx int) (string, error) {
321375
secretName := strings.Join([]string{tfstateKey, c.workspace, c.nameSuffix}, "-")
322376

377+
if idx > 0 {
378+
secretName = fmt.Sprintf("%s-part%d", secretName, idx)
379+
}
380+
323381
errs := validation.IsDNS1123Subdomain(secretName)
324382
if len(errs) > 0 {
325383
k8sInfo := `
@@ -333,7 +391,7 @@ The workspace name and key must adhere to Kubernetes naming conventions.`
333391
}
334392

335393
func (c *RemoteClient) createLeaseName() (string, error) {
336-
n, err := c.createSecretName()
394+
n, err := c.createSecretName(0)
337395
if err != nil {
338396
return "", err
339397
}
@@ -352,14 +410,18 @@ func compressState(data []byte) ([]byte, error) {
352410
return b.Bytes(), nil
353411
}
354412

355-
func uncompressState(data string) ([]byte, error) {
356-
decode, err := base64.StdEncoding.DecodeString(data)
357-
if err != nil {
358-
return nil, err
413+
func uncompressState(data []string) ([]byte, error) {
414+
var rawData []byte
415+
for _, chunk := range data {
416+
decode, err := base64.StdEncoding.DecodeString(chunk)
417+
if err != nil {
418+
return nil, err
419+
}
420+
rawData = append(rawData, decode...)
359421
}
360422

361423
b := new(bytes.Buffer)
362-
gz, err := gzip.NewReader(bytes.NewReader(decode))
424+
gz, err := gzip.NewReader(bytes.NewReader(rawData))
363425
if err != nil {
364426
return nil, err
365427
}

0 commit comments

Comments
 (0)