// Copyright 2019 The Kubeflow Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fetch from 'node-fetch'; import { awsInstanceProfileCredentials, isAWSS3Endpoint } from './aws-helper'; // mock node-fetch module jest.mock('node-fetch'); function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } beforeEach(() => { awsInstanceProfileCredentials.reset(); jest.clearAllMocks(); }); describe('awsInstanceProfileCredentials', () => { const mockedFetch: jest.Mock = fetch as any; describe('getCredentials', () => { it('retrieves, caches, and refreshes the AWS EC2 instance profile and session credentials everytime it is called.', async () => { let count = 0; const expectedCredentials = [ { AccessKeyId: 'AccessKeyId', Code: 'Success', Expiration: new Date(Date.now() + 1000).toISOString(), // expires 1 sec later LastUpdated: '2019-12-17T10:55:38Z', SecretAccessKey: 'SecretAccessKey', Token: 'SessionToken', Type: 'AWS-HMAC', }, { AccessKeyId: 'AccessKeyId2', Code: 'Success', Expiration: new Date(Date.now() + 10000).toISOString(), // expires 10 sec later LastUpdated: '2019-12-17T10:55:38Z', SecretAccessKey: 'SecretAccessKey2', Token: 'SessionToken2', Type: 'AWS-HMAC', }, ]; const mockFetch = (url: string) => { if (url === 'http://169.254.169.254/latest/meta-data/iam/security-credentials/') { return Promise.resolve({ text: () => Promise.resolve('some_iam_role') }); } return Promise.resolve({ json: () => Promise.resolve(expectedCredentials[count++]), }); }; mockedFetch.mockImplementation(mockFetch); // expect to get cred from ec2 instance metadata store expect(await awsInstanceProfileCredentials.getCredentials()).toBe(expectedCredentials[0]); // expect to call once for profile name, and once for credential expect(mockedFetch.mock.calls.length).toBe(2); // expect to get same credential as it has not expire expect(await awsInstanceProfileCredentials.getCredentials()).toBe(expectedCredentials[0]); // expect to not to have any more calls expect(mockedFetch.mock.calls.length).toBe(2); // let credential expire await sleep(1500); // expect to get new cred as old one expire expect(await awsInstanceProfileCredentials.getCredentials()).toBe(expectedCredentials[1]); // expect to get same cred as it has not expire expect(await awsInstanceProfileCredentials.getCredentials()).toBe(expectedCredentials[1]); }); it('fails gracefully if there is no instance profile.', async () => { const mockFetch = (url: string) => { if (url === 'http://169.254.169.254/latest/meta-data/iam/security-credentials/') { return Promise.resolve({ text: () => Promise.resolve('') }); } return Promise.reject('Unknown error'); }; mockedFetch.mockImplementation(mockFetch); expect(await awsInstanceProfileCredentials.ok()).toBeFalsy(); expect(async () => await awsInstanceProfileCredentials.getCredentials()).not.toThrow(); expect(await awsInstanceProfileCredentials.getCredentials()).toBeUndefined(); }); it('fails gracefully if there is no metadata store.', async () => { const mockFetch = (_: string) => { return Promise.reject('Unknown error'); }; mockedFetch.mockImplementation(mockFetch); expect(await awsInstanceProfileCredentials.ok()).toBeFalsy(); expect(async () => await awsInstanceProfileCredentials.getCredentials()).not.toThrow(); expect(await awsInstanceProfileCredentials.getCredentials()).toBeUndefined(); }); }); }); describe('isS3Endpoint', () => { it('checks a valid s3 endpoint', () => { expect(isAWSS3Endpoint('s3.amazonaws.com')).toBe(true); }); it('checks a valid s3 regional endpoint', () => { expect(isAWSS3Endpoint('s3.dualstack.us-east-1.amazonaws.com')).toBe(true); }); it('checks a valid s3 cn endpoint', () => { expect(isAWSS3Endpoint('s3.cn-north-1.amazonaws.com.cn')).toBe(true); }); it('checks a valid s3 fips GovCloud endpoint', () => { expect(isAWSS3Endpoint('s3-fips.us-gov-west-1.amazonaws.com')).toBe(true); }); it('checks a valid s3 PrivateLink endpoint', () => { expect(isAWSS3Endpoint('vpce-1a2b3c4d-5e6f.s3.us-east-1.vpce.amazonaws.com')).toBe(true); }); it('checks an invalid s3 endpoint', () => { expect(isAWSS3Endpoint('amazonaws.com')).toBe(false); }); it('checks non-s3 endpoint', () => { expect(isAWSS3Endpoint('minio.kubeflow')).toBe(false); }); });