Skip to content

Extend Minio Health Indicator to Check for Default Bucket

Excellent point. It's good practice to provide flexibility and handle both singular and plural forms for configuration options. This makes the module more intuitive to use.

I've revised the GitLab issue to incorporate defaultBucket and defaultBuckets as separate, optional properties.


Extend Minio Health Indicator to Check for Default Bucket(s)

Description

The current implementation of the MinioHealthIndicator in @rxap/nest-minio only checks if the client can list buckets. This is a good first step, but it doesn't guarantee that the application will be able to use Minio if a default bucket is required and doesn't exist.

This issue proposes extending the health indicator to check for the existence of one or more default buckets if the MINIO_DEFAULT_BUCKETS or MINIO_BUCKET_NAME environment variables are defined. This will be accomplished by extending the module's options, updating the options loader, and then using the injected options in the health indicator.

Proposed Solution

The solution involves a few key changes to integrate the bucket check into the existing module structure.

1. Extend Module Options and Validation

First, we need to account for the new environment variables in our validation schema and module options.

minio/src/lib/minio-validation-schema.ts

Add the MINIO_DEFAULT_BUCKETS and MINIO_BUCKET_NAME to the Joi validation schema.

// ...
export function minioValidationSchema(environment: Environment, {
  //...
  bucketName = '',
}: {
  // ...
  bucketName?: string;
} = {}) {

  const schema: SchemaMap = {};
  // ...
  schema['MINIO_BUCKET_NAME'] = Joi.string().default(bucketName);
  schema['MINIO_DEFAULT_BUCKETS'] = Joi.string();

  return schema;
}

minio/src/lib/configurable-module-builder.ts (or a new types file)

We'll define a new options interface that includes the bucket names.

import { ClientOptions } from 'minio';

export interface MinioModuleOptions extends ClientOptions {
  defaultBucket?: string;
  defaultBuckets?: string[];
}

2. Update the Options Loader

Next, we'll modify the MinioModuleOptionsLoader to read our new environment variables and populate the defaultBucket and defaultBuckets properties in the options object.

minio/src/lib/minio-module-options-loader.ts

import { MinioModuleOptions } from './configurable-module-builder'; // Or wherever the interface is defined

// ...

@Injectable()
export class MinioModuleOptionsLoader
  implements ConfigurableModuleOptionsFactory<MinioModuleOptions, 'create'> {

  // ...

  create(): MinioModuleOptions {
    const useSSL = this.config.get<boolean>('MINIO_USE_SSL', false);
    // ...

    const defaultBucket = this.config.get<string>('MINIO_BUCKET_NAME');
    const defaultBuckets = this.config.get<string>('MINIO_DEFAULT_BUCKETS')?.split(',').map(b => b.trim());

    return {
      endPoint: this.config.get('MINIO_END_POINT', 'minio'),
      port: this.config.get('MINIO_PORT', 9000),
      useSSL: useSSL,
      accessKey: this.config.get('MINIO_ACCESS_KEY', 'minioadmin'),
      secretKey: this.config.get('MINIO_SECRET_KEY', 'minioadmin'),
      transportAgent: agent,
      defaultBucket,
      defaultBuckets,
    };
  }
}

3. Update the Health Indicator

Finally, we'll inject the MINIO_OPTIONS into the MinioHealthIndicator and use the defaultBucket and defaultBuckets properties to perform the health check.

minio/src/lib/minio.health-indicator.ts

import {
  Inject,
  Injectable,
  Logger,
} from '@nestjs/common';
import {
  HealthCheckError,
  HealthIndicator,
  HealthIndicatorResult,
} from '@nestjs/terminus';
import { MinioService } from './minio.service';
import { MINIO_OPTIONS } from './tokens';
import { MinioModuleOptions } from './configurable-module-builder'; // Or wherever the interface is defined

/**
 * Class representing a MinioHealthIndicator.
 * @class
 * @inheritDoc
 */
@Injectable()
export class MinioHealthIndicator extends HealthIndicator {

  @Inject(MinioService)
  public minioService!: MinioService;

  @Inject(MINIO_OPTIONS)
  private readonly minioOptions!: MinioModuleOptions;

  @Inject(Logger)
  public logger!: Logger;

  public async isHealthy(): Promise<HealthIndicatorResult> {
    const bucketsToCheck = new Set<string>();

    if (this.minioOptions.defaultBucket) {
      bucketsToCheck.add(this.minioOptions.defaultBucket);
    }

    if (this.minioOptions.defaultBuckets) {
      this.minioOptions.defaultBuckets.forEach(bucket => {
        bucketsToCheck.add(bucket);
      });
    }

    if (bucketsToCheck.size > 0) {
      try {
        for (const bucket of bucketsToCheck) {
          const bucketExists = await this.minioService.bucketExists(bucket);
          if (!bucketExists) {
            throw new HealthCheckError(
              `Minio bucket "${bucket}" does not exist`,
              this.getStatus('minio', false, {
                message: `Bucket ${bucket} not found`,
              }),
            );
          }
        }
        return this.getStatus('minio', true);
      } catch (error: any) {
        this.logger.error(`Failed to check minio buckets`, error.stack, 'MinioHealthIndicator');
        throw new HealthCheckError(
          'Failed to check minio buckets',
          this.getStatus('minio', false),
        );
      }
    }

    // Fallback to original behavior if no default buckets are defined
    try {
      const response = await this.minioService.listBuckets();
      if (Array.isArray(response)) {
        return this.getStatus('minio', true);
      }
    } catch (error: any) {
      if ('message' in error && typeof error.message === 'string') {
        this.logger.error(`Failed to list minio buckets (code=${error.code}): ${ error.message || '[empty error message]' }`, error.stack, 'MinioHealthIndicator');
      } else {
        this.logger.error(`Failed to list minio buckets: ${ error }`, undefined, 'MinioHealthIndicator');
      }
    }
    throw new HealthCheckError(
      'Minio health check failed',
      this.getStatus('minio', false),
    );
  }
}