Skip to content

Commit fcc7676

Browse files
sw-choCopilot
andauthored
feat(sync): Support Kerberos authentication for HDFS sync (#101)
* feat(sync): Support Kerberos authentication for HDFS sync * Fix error message Co-authored-by: Copilot <[email protected]> * Add comments to kerberos related fields Co-authored-by: Copilot <[email protected]> * Add test case for HDFS-to-HDFS sync rejection * Prevent setting both KRB5Keytab and KRB5KeytabBase64 * chore: sync generated deepcopy with struct field changes --------- Co-authored-by: Copilot <[email protected]>
1 parent f0dc02b commit fcc7676

File tree

9 files changed

+948
-0
lines changed

9 files changed

+948
-0
lines changed

api/v1/sync_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ type SyncSinkExternal struct {
7777
SecretKey SyncSinkValue `json:"secretKey,omitempty"`
7878
FilesFrom *SyncFilesFrom `json:"filesFrom,omitempty"`
7979

80+
// KRB5Keytab specifies the path to the Kerberos keytab file for HDFS authentication.
81+
KRB5Keytab SyncSinkValue `json:"krb5Keytab,omitempty"`
82+
// KRB5KeytabBase64 contains the base64-encoded Kerberos keytab content for HDFS authentication.
83+
KRB5KeytabBase64 SyncSinkValue `json:"krb5KeytabBase64,omitempty"`
84+
// KRB5Principal is the Kerberos principal name used for HDFS authentication.
85+
KRB5Principal SyncSinkValue `json:"krb5Principal,omitempty"`
86+
8087
ExtraVolumes []ExtraVolume `json:"extraVolumes,omitempty"`
8188
}
8289

api/v1/zz_generated.deepcopy.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/juicefs.io_cronsyncs.yaml

Lines changed: 360 additions & 0 deletions
Large diffs are not rendered by default.

config/crd/bases/juicefs.io_syncs.yaml

Lines changed: 360 additions & 0 deletions
Large diffs are not rendered by default.

internal/controller/sync_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
119119
return ctrl.Result{}, r.updateStatus(ctx, sync)
120120
}
121121

122+
// Reject sync when both from and to are HDFS
123+
if strings.HasPrefix(from.Uri, "hdfs://") && strings.HasPrefix(to.Uri, "hdfs://") {
124+
err := fmt.Errorf("HDFS-to-HDFS sync is not supported")
125+
l.Error(err, "")
126+
sync.Status.Phase = juicefsiov1.SyncPhaseFailed
127+
sync.Status.Reason = err.Error()
128+
return ctrl.Result{}, r.updateStatus(ctx, sync)
129+
}
130+
122131
if strings.HasSuffix(from.Uri, "/") != strings.HasSuffix(to.Uri, "/") {
123132
err := fmt.Errorf("FROM and TO should both end with path separator or not")
124133
l.Error(err, "")

internal/controller/sync_controller_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,82 @@ var _ = Describe("Sync Controller", func() {
8181
// Example: If you expect a certain status condition after reconciliation, verify it here.
8282
})
8383
})
84+
85+
Context("When rejecting HDFS-to-HDFS sync", func() {
86+
const hdfsToHdfsResourceName = "hdfs-to-hdfs-sync"
87+
88+
ctx := context.Background()
89+
90+
typeNamespacedName := types.NamespacedName{
91+
Name: hdfsToHdfsResourceName,
92+
Namespace: "default",
93+
}
94+
95+
BeforeEach(func() {
96+
By("creating a Sync resource with both from and to as HDFS")
97+
resource := &juicefsiov1.Sync{
98+
ObjectMeta: metav1.ObjectMeta{
99+
Name: hdfsToHdfsResourceName,
100+
Namespace: "default",
101+
},
102+
Spec: juicefsiov1.SyncSpec{
103+
Image: "juicedata/mount:ce-v1.3.0",
104+
From: juicefsiov1.SyncSink{
105+
External: &juicefsiov1.SyncSinkExternal{
106+
Uri: "hdfs://example.com/user/source/",
107+
KRB5Principal: juicefsiov1.SyncSinkValue{
108+
Value: "hdfs/[email protected]",
109+
},
110+
KRB5KeytabBase64: juicefsiov1.SyncSinkValue{
111+
Value: "dGVzdC1rZXl0YWItYmFzZTY0",
112+
},
113+
},
114+
},
115+
To: juicefsiov1.SyncSink{
116+
External: &juicefsiov1.SyncSinkExternal{
117+
Uri: "hdfs://example.com/user/destination/",
118+
KRB5Principal: juicefsiov1.SyncSinkValue{
119+
Value: "hdfs/[email protected]",
120+
},
121+
KRB5KeytabBase64: juicefsiov1.SyncSinkValue{
122+
Value: "dGVzdC1rZXl0YWItYmFzZTY0",
123+
},
124+
},
125+
},
126+
},
127+
}
128+
err := k8sClient.Get(ctx, typeNamespacedName, &juicefsiov1.Sync{})
129+
if err != nil && errors.IsNotFound(err) {
130+
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
131+
}
132+
})
133+
134+
AfterEach(func() {
135+
By("cleaning up the HDFS-to-HDFS sync resource")
136+
resource := &juicefsiov1.Sync{}
137+
err := k8sClient.Get(ctx, typeNamespacedName, resource)
138+
if err == nil {
139+
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
140+
}
141+
})
142+
143+
It("should reject sync and set status to Failed with appropriate reason", func() {
144+
By("reconciling the HDFS-to-HDFS sync resource")
145+
controllerReconciler := &SyncReconciler{
146+
Client: k8sClient,
147+
Scheme: k8sClient.Scheme(),
148+
}
149+
150+
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
151+
NamespacedName: typeNamespacedName,
152+
})
153+
Expect(err).NotTo(HaveOccurred())
154+
155+
By("verifying the sync status is set to Failed")
156+
sync := &juicefsiov1.Sync{}
157+
Expect(k8sClient.Get(ctx, typeNamespacedName, sync)).To(Succeed())
158+
Expect(sync.Status.Phase).To(Equal(juicefsiov1.SyncPhaseFailed))
159+
Expect(sync.Status.Reason).To(ContainSubstring("HDFS-to-HDFS sync is not supported"))
160+
})
161+
})
84162
})

pkg/builder/sync_secret.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ func genExternalSecretData(external *juicefsiov1.SyncSinkExternal, suffix string
5757
if external.SecretKey.Value != "" {
5858
data[fmt.Sprintf("%s_SECRET_KEY", suffix)] = external.SecretKey.Value
5959
}
60+
// HDFS Kerberos credentials
61+
if external.KRB5Keytab.Value != "" {
62+
data[fmt.Sprintf("%s_KRB5KEYTAB", suffix)] = external.KRB5Keytab.Value
63+
}
64+
if external.KRB5KeytabBase64.Value != "" {
65+
data[fmt.Sprintf("%s_KRB5KEYTAB_BASE64", suffix)] = external.KRB5KeytabBase64.Value
66+
}
67+
if external.KRB5Principal.Value != "" {
68+
data[fmt.Sprintf("%s_KRB5PRINCIPAL", suffix)] = external.KRB5Principal.Value
69+
}
6070
return data
6171
}
6272

pkg/utils/sync.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,30 @@ func parseExternalSyncSink(external *juicefsiov1.SyncSinkExternal, syncName, ref
114114
if len(pss.Envs) == 2 {
115115
ep.User = url.UserPassword(fmt.Sprintf("$%s", ak), fmt.Sprintf("$%s", sk))
116116
}
117+
if ep.Scheme == "hdfs" {
118+
krb5Keytab := fmt.Sprintf("EXTERNAL_%s_%s", strings.ToUpper(ref), "KRB5KEYTAB")
119+
krb5KeytabBase64 := fmt.Sprintf("EXTERNAL_%s_%s", strings.ToUpper(ref), "KRB5KEYTAB_BASE64")
120+
krb5Principal := fmt.Sprintf("EXTERNAL_%s_%s", strings.ToUpper(ref), "KRB5PRINCIPAL")
121+
122+
hasKeytab := external.KRB5Keytab.Value != "" || external.KRB5Keytab.ValueFrom != nil
123+
hasKeytabBase64 := external.KRB5KeytabBase64.Value != "" || external.KRB5KeytabBase64.ValueFrom != nil
124+
if hasKeytab && hasKeytabBase64 {
125+
return nil, fmt.Errorf("krb5Keytab and krb5KeytabBase64 are mutually exclusive. Please specify only one keytab method for the %s field", strings.ToUpper(ref))
126+
}
127+
if hasKeytab {
128+
pss.Envs = append(pss.Envs, parseSinkValueToEnv(external.KRB5Keytab, syncName, krb5Keytab)...)
129+
// Export to standard env var name for juicefs sync compatibility
130+
pss.PrepareCommand += fmt.Sprintf("export %s=$%s\n", "KRB5KEYTAB", krb5Keytab)
131+
}
132+
if hasKeytabBase64 {
133+
pss.Envs = append(pss.Envs, parseSinkValueToEnv(external.KRB5KeytabBase64, syncName, krb5KeytabBase64)...)
134+
pss.PrepareCommand += fmt.Sprintf("export %s=$%s\n", "KRB5KEYTAB_BASE64", krb5KeytabBase64)
135+
}
136+
if external.KRB5Principal.Value != "" || external.KRB5Principal.ValueFrom != nil {
137+
pss.Envs = append(pss.Envs, parseSinkValueToEnv(external.KRB5Principal, syncName, krb5Principal)...)
138+
pss.PrepareCommand += fmt.Sprintf("export %s=$%s\n", "KRB5PRINCIPAL", krb5Principal)
139+
}
140+
}
117141
pss.Uri = ep.String()
118142
pss.ExtraVolumes = external.ExtraVolumes
119143
return pss, nil

pkg/utils/sync_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,103 @@ func TestParseSyncSink(t *testing.T) {
318318
want: nil,
319319
wantErr: true,
320320
},
321+
{
322+
name: "HDFS sink with KRB5Principal and KRB5Keytab",
323+
sink: juicefsiov1.SyncSink{
324+
External: &juicefsiov1.SyncSinkExternal{
325+
Uri: "hdfs://example.com/user/test/",
326+
KRB5Principal: juicefsiov1.SyncSinkValue{
327+
Value: "hdfs/[email protected]",
328+
},
329+
KRB5Keytab: juicefsiov1.SyncSinkValue{
330+
Value: "/root/keytab/keytab",
331+
},
332+
},
333+
},
334+
syncName: "sync4",
335+
ref: "FROM",
336+
want: &juicefsiov1.ParsedSyncSink{
337+
Uri: "hdfs://example.com/user/test/",
338+
Envs: []corev1.EnvVar{
339+
{
340+
Name: "EXTERNAL_FROM_KRB5KEYTAB",
341+
ValueFrom: &corev1.EnvVarSource{
342+
SecretKeyRef: &corev1.SecretKeySelector{
343+
LocalObjectReference: corev1.LocalObjectReference{
344+
Name: common.GenSyncSecretName("sync4"),
345+
},
346+
Key: "EXTERNAL_FROM_KRB5KEYTAB",
347+
},
348+
},
349+
},
350+
{
351+
Name: "EXTERNAL_FROM_KRB5PRINCIPAL",
352+
ValueFrom: &corev1.EnvVarSource{
353+
SecretKeyRef: &corev1.SecretKeySelector{
354+
LocalObjectReference: corev1.LocalObjectReference{
355+
Name: common.GenSyncSecretName("sync4"),
356+
},
357+
Key: "EXTERNAL_FROM_KRB5PRINCIPAL",
358+
},
359+
},
360+
},
361+
},
362+
PrepareCommand: "export KRB5KEYTAB=$EXTERNAL_FROM_KRB5KEYTAB\nexport KRB5PRINCIPAL=$EXTERNAL_FROM_KRB5PRINCIPAL\n",
363+
},
364+
wantErr: false,
365+
},
366+
{
367+
name: "HDFS sink with KRB5Principal and KRB5KeytabBase64",
368+
sink: juicefsiov1.SyncSink{
369+
External: &juicefsiov1.SyncSinkExternal{
370+
Uri: "hdfs://example.com/user/test/",
371+
KRB5Principal: juicefsiov1.SyncSinkValue{
372+
Value: "hdfs/[email protected]",
373+
},
374+
KRB5KeytabBase64: juicefsiov1.SyncSinkValue{
375+
ValueFrom: &corev1.EnvVarSource{
376+
SecretKeyRef: &corev1.SecretKeySelector{
377+
LocalObjectReference: corev1.LocalObjectReference{
378+
Name: "keytabbase64-secret",
379+
},
380+
Key: "KRB5KEYTAB_BASE64",
381+
},
382+
},
383+
},
384+
},
385+
},
386+
syncName: "sync5",
387+
ref: "FROM",
388+
want: &juicefsiov1.ParsedSyncSink{
389+
Uri: "hdfs://example.com/user/test/",
390+
Envs: []corev1.EnvVar{
391+
{
392+
Name: "EXTERNAL_FROM_KRB5KEYTAB_BASE64",
393+
ValueFrom: &corev1.EnvVarSource{
394+
SecretKeyRef: &corev1.SecretKeySelector{
395+
LocalObjectReference: corev1.LocalObjectReference{
396+
Name: "keytabbase64-secret",
397+
},
398+
Key: "KRB5KEYTAB_BASE64",
399+
},
400+
},
401+
},
402+
{
403+
Name: "EXTERNAL_FROM_KRB5PRINCIPAL",
404+
ValueFrom: &corev1.EnvVarSource{
405+
SecretKeyRef: &corev1.SecretKeySelector{
406+
LocalObjectReference: corev1.LocalObjectReference{
407+
Name: common.GenSyncSecretName("sync5"),
408+
},
409+
Key: "EXTERNAL_FROM_KRB5PRINCIPAL",
410+
},
411+
},
412+
},
413+
},
414+
PrepareCommand: "export KRB5KEYTAB_BASE64=$EXTERNAL_FROM_KRB5KEYTAB_BASE64\nexport KRB5PRINCIPAL=$EXTERNAL_FROM_KRB5PRINCIPAL\n",
415+
},
416+
wantErr: false,
417+
},
321418
}
322419

323420
for _, tt := range tests {

0 commit comments

Comments
 (0)