Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 512edd4

Browse files
starpitk8s-ci-robot
authored andcommitted
feat: Add support for "slash tmp" to s3 mounts
This PR does so for IBM mounts part of #7721
1 parent c849168 commit 512edd4

File tree

12 files changed

+311
-75
lines changed

12 files changed

+311
-75
lines changed

packages/core/src/core/userdata.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,18 @@ export const setPreference = async (key: string, value: string): Promise<string>
183183
await fsyncPreferences(prefs)
184184
return value
185185
}
186+
187+
export async function getOrSetPreference(
188+
key: string,
189+
defaultValue: string | (() => string | Promise<string>)
190+
): Promise<string> {
191+
const prefs = await preferences()
192+
if (prefs[key]) {
193+
return prefs[key]
194+
} else {
195+
const value = typeof defaultValue === 'string' ? defaultValue : await defaultValue()
196+
prefs[key] = value
197+
await fsyncPreferences(prefs)
198+
return value
199+
}
200+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export { commandsOffered as commandsOfferedByPlugin, userHome as pluginUserHome
250250

251251
// Settings
252252
export { userDataDir, uiThemes } from './core/settings'
253+
export { getOrSetPreference, getPreference, setPreference } from './core/userdata'
253254

254255
// Storage for user data
255256
export { default as Store } from './models/store'
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2021 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { S3Provider } from '@kui-shell/plugin-s3'
18+
19+
import Config from './model/Config'
20+
import Geos, { GeoDefaults } from './model/geos'
21+
22+
export default class IBMCloudS3Provider implements S3Provider {
23+
public readonly endPoint: string
24+
public readonly directEndPoint: string
25+
26+
public readonly region: string
27+
public readonly accessKey: string
28+
public readonly secretKey: string
29+
30+
public readonly understandsFolders = true
31+
public readonly bucketNameNSlashes: 1
32+
public readonly listBuckets: S3Provider['listBuckets']
33+
34+
public isDefault: boolean
35+
36+
public constructor(
37+
private readonly geo: string,
38+
public readonly mountName: string,
39+
config?: Config,
40+
public readonly error?: Error
41+
) {
42+
const accessKey = config ? config.AccessKeyID : undefined
43+
const secretKey = config ? config.SecretAccessKey : undefined
44+
45+
this.endPoint = Geos[geo] || (config ? config.endpointForKui : undefined)
46+
this.region = geo
47+
this.accessKey = accessKey
48+
this.secretKey = secretKey
49+
50+
// fast-path endpoint when executing in a cloud job?
51+
// e.g. s3.ap.cloud-object-storage.appdomain.cloud -> s3.direct.ap.cloud-object-storage.appdomain.cloud
52+
this.directEndPoint = this.endPoint.replace(/^s3\./, 's3.direct.')
53+
54+
const defaultRegion = GeoDefaults[config['Default Region']] || config['Default Region']
55+
this.isDefault = config && geo === defaultRegion
56+
57+
// use the closest available endpoint for listBuckets, since it is geo-agnostic
58+
this.listBuckets = {
59+
endPoint: config ? config.endpointForKui : Geos[geo],
60+
region: geo,
61+
accessKey,
62+
secretKey
63+
}
64+
}
65+
66+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
67+
public bucketFilter({ name, locationConstraint }: { name: string; locationConstraint: string }): boolean {
68+
return !locationConstraint || locationConstraint.startsWith(this.geo)
69+
}
70+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2021 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** So that the user can interface via /s3/ibm; this is the "ibm" part of that path */
18+
const baseMountName = 'ibm'
19+
20+
export default baseMountName

plugins/plugin-s3/ibm/src/s3provider.ts

Lines changed: 15 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,64 +15,16 @@
1515
*/
1616

1717
import { REPL, eventChannelUnsafe } from '@kui-shell/core'
18-
import { S3Provider, ProviderInitializer, UnsupportedS3ProviderError } from '@kui-shell/plugin-s3'
18+
import { ProviderInitializer, UnsupportedS3ProviderError } from '@kui-shell/plugin-s3'
1919

20+
import Geos from './model/geos'
2021
import Config from './model/Config'
2122
import updateChannel from './channel'
2223
import { isGoodConfig } from './controller/local'
23-
import Geos, { GeoDefaults } from './model/geos'
2424

25-
const baseMountName = 'ibm'
26-
27-
class IBMCloudS3Provider implements S3Provider {
28-
public readonly endPoint: string
29-
public readonly directEndPoint: string
30-
31-
public readonly region: string
32-
public readonly accessKey: string
33-
public readonly secretKey: string
34-
35-
public readonly understandsFolders = true
36-
public readonly bucketNameNSlashes: 1
37-
public readonly listBuckets: S3Provider['listBuckets']
38-
39-
public isDefault: boolean
40-
41-
public constructor(
42-
private readonly geo: string,
43-
public readonly mountName: string,
44-
config?: Config,
45-
public readonly error?: Error
46-
) {
47-
const accessKey = config ? config.AccessKeyID : undefined
48-
const secretKey = config ? config.SecretAccessKey : undefined
49-
50-
this.endPoint = Geos[geo] || (config ? config.endpointForKui : undefined)
51-
this.region = geo
52-
this.accessKey = accessKey
53-
this.secretKey = secretKey
54-
55-
// fast-path endpoint when executing in a cloud job?
56-
// e.g. s3.ap.cloud-object-storage.appdomain.cloud -> s3.direct.ap.cloud-object-storage.appdomain.cloud
57-
this.directEndPoint = this.endPoint.replace(/^s3\./, 's3.direct.')
58-
59-
const defaultRegion = GeoDefaults[config['Default Region']] || config['Default Region']
60-
this.isDefault = config && geo === defaultRegion
61-
62-
// use the closest available endpoint for listBuckets, since it is geo-agnostic
63-
this.listBuckets = {
64-
endPoint: config ? config.endpointForKui : Geos[geo],
65-
region: geo,
66-
accessKey,
67-
secretKey
68-
}
69-
}
70-
71-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
72-
public bucketFilter({ name, locationConstraint }: { name: string; locationConstraint: string }): boolean {
73-
return !locationConstraint || locationConstraint.startsWith(this.geo)
74-
}
75-
}
25+
import baseMountName from './baseMountName'
26+
import IBMCloudS3Provider from './IBMCloudS3Provider'
27+
import extraProvidersForDefaultRegion from './specialMounts'
7628

7729
/** Listening for reconfigs? */
7830
let listeningAlready = false
@@ -86,6 +38,7 @@ async function fetchConfig(repl: REPL): Promise<void | Config> {
8638
return (await currentConfig).content
8739
}
8840

41+
/** Initialize an S3Provider for the given geo */
8942
async function init(geo: string, mountName: string, repl: REPL, reinit: () => void) {
9043
try {
9144
if (!listeningAlready) {
@@ -110,13 +63,14 @@ async function init(geo: string, mountName: string, repl: REPL, reinit: () => vo
11063
// TODO: isn't there a race here?
11164
config['Default Region'] = await repl.qexec('ibmcloud cos config region default')
11265
}
66+
11367
const provider = new IBMCloudS3Provider(geo, mountName, config)
68+
69+
// special handling for default geo
11470
if (provider.isDefault) {
115-
// add an /s3/ibm/default mount point
116-
const defaultProvider = new IBMCloudS3Provider(geo, 'ibm/default', config)
117-
defaultProvider.isDefault = true
71+
// e.g. add an /s3/ibm/default mount point
11872
provider.isDefault = false
119-
return [defaultProvider, provider]
73+
return [...(await extraProvidersForDefaultRegion(geo, config)), provider]
12074
} else {
12175
return provider
12276
}
@@ -126,6 +80,10 @@ async function init(geo: string, mountName: string, repl: REPL, reinit: () => vo
12680
}
12781
}
12882

83+
/**
84+
* We want one ProviderInitializer per geo
85+
*
86+
*/
12987
const initializer: ProviderInitializer[] = Object.keys(Geos)
13088
// .filter(_ => !/-geo$/.test(_)) // don't manifest geo endpoints in the VFS
13189
.map(geo => {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { v4 } from 'uuid'
18+
import { getOrSetPreference } from '@kui-shell/core'
19+
20+
import Config from './model/Config'
21+
import baseMountName from './baseMountName'
22+
import IBMCloudS3Provider from './IBMCloudS3Provider'
23+
24+
/** The /s3/ibm/default mount point */
25+
function defaultProvider(geo: string, config: Config) {
26+
const provider = new IBMCloudS3Provider(geo, `${baseMountName}/default`, config)
27+
provider.isDefault = true
28+
return provider
29+
}
30+
31+
class Pseudo extends IBMCloudS3Provider {
32+
public constructor(geo: string, mountName: string, config: Config, public readonly subdir: string) {
33+
super(geo, mountName, config)
34+
}
35+
}
36+
37+
/** The /s3/ibm/tmp mount point */
38+
async function pseudoProvider(geo: string, config: Config, pseudo: string) {
39+
const subdir = await getOrSetPreference(`org.kubernetes-sigs.kui/s3/pseudomount/${pseudo}`, `${pseudo}-${v4()}`)
40+
const provider = new Pseudo(geo, `${baseMountName}/tmp`, config, subdir)
41+
return provider
42+
}
43+
44+
export default function extraProvidersForDefaultRegion(geo: string, config: Config) {
45+
return Promise.all([
46+
defaultProvider(geo, config),
47+
pseudoProvider(geo, config, 'bin'),
48+
pseudoProvider(geo, config, 'tmp')
49+
])
50+
}

plugins/plugin-s3/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ export {
2121
ProviderInitializer,
2222
UnsupportedS3ProviderError
2323
} from './providers'
24-
export { minioConfig, mounts as getCurrentMounts, Mount } from './vfs/responders'
24+
2525
export { default as eventBus } from './vfs/events'
26+
export { minioConfig, mounts as getCurrentMounts, Mount } from './vfs/responders'

plugins/plugin-s3/src/providers/model.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@ type BucketFilter = (bucket: { name: string; locationConstraint: string }) => bo
2222

2323
type Provider = ClientOptions & {
2424
/** Optimized path for job execution? */
25-
directEndPoint?: string
25+
readonly directEndPoint?: string
2626

27-
mountName: string
28-
region?: string
29-
error?: Error
30-
bucketFilter?: BucketFilter
31-
listBuckets?: ClientOptions
32-
understandsFolders?: boolean
33-
isDefault?: boolean
27+
readonly mountName: string
28+
readonly region?: string
29+
readonly error?: Error
30+
readonly bucketFilter?: BucketFilter
31+
readonly listBuckets?: ClientOptions
32+
readonly understandsFolders?: boolean
33+
readonly isDefault?: boolean
3434

3535
/** Does this provider allows access only to public buckets? */
36-
publicOnly?: boolean
36+
readonly publicOnly?: boolean
37+
38+
/** Is this mount a subirectory mount of an existing mount? */
39+
readonly subdir?: string
3740
}
3841
export default Provider
3942

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { tmpdir } from 'os'
18+
import { Common } from '@kui-shell/test'
19+
20+
import S3Utils, { README, README3 } from './util'
21+
22+
export default function pseudo(this: Common.ISuite) {
23+
// bind the helper routines
24+
const { copyFromS3, copyToS3, copyWithinS3, lsExpecting404, rm } = S3Utils.bind(this)()
25+
26+
// v4 to get unique names; some s3 backends can't recreate a
27+
// bucketName immediately after deleting it
28+
const bucketName1 = 'tmp'
29+
const bucketName2 = 'bin'
30+
31+
copyToS3(bucketName1)
32+
copyWithinS3(bucketName1, '*.md', bucketName2)
33+
copyWithinS3(bucketName1, README, bucketName2, README3)
34+
copyFromS3(bucketName2, README3, tmpdir()) // copy out the intra-s3 copy
35+
rm(bucketName1, README)
36+
rm(bucketName2, README)
37+
lsExpecting404(bucketName1)
38+
lsExpecting404(bucketName2)
39+
}

plugins/plugin-s3/src/test/s3/s3.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import cd from './cd'
2020
import S3Utils from './util'
2121
import basics from './basics'
2222
import rimraf from './rimraf'
23+
import pseudo from './pseudo'
2324
import folders from './folders'
2425
import wildcardRimraf from './wildcard-rimraf'
2526

@@ -37,6 +38,16 @@ if (process.env.NEEDS_MINIO) {
3738
.catch(Common.oops(this, true)))
3839
})
3940

41+
xdescribe('s3 vfs pseudo mounts', function(this: Common.ISuite) {
42+
before(Common.before(this))
43+
after(Common.after(this))
44+
45+
const { init } = S3Utils.bind(this)()
46+
47+
init()
48+
pseudo.bind(this)()
49+
})
50+
4051
describe('s3 vfs folders', function(this: Common.ISuite) {
4152
before(Common.before(this))
4253
after(Common.after(this))

0 commit comments

Comments
 (0)