Skip to content

Commit

Permalink
Add CertificateSigningRequests Resource (#648)
Browse files Browse the repository at this point in the history
This commit adds support for the `CertificateSigningRequest` resource.
This means that users can view CSRs within the app. It is also possible
to approve or deny a CSR within the app.

We also added a new `DateTimeExtension`, to format the time in a format
required by the Kubernetes API server. For this we also moved the
`_rFC3339Nano` function required by the Flux plugin to the newly added
extension.
  • Loading branch information
ricoberger committed May 12, 2024
1 parent e830a26 commit eab8cb5
Show file tree
Hide file tree
Showing 9 changed files with 613 additions and 48 deletions.
13 changes: 13 additions & 0 deletions assets/resources/certificatesigningrequests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions lib/utils/resources.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,26 @@ String formatBytes(int size, {int round = 2}) {

return '$result ${affixes[affix]}';
}

/// [formatSeconds] formats the provided [seconds] into a human readable format.
String formatSeconds(int? seconds) {
if (seconds == null) {
return '-';
}

final duration = Duration(seconds: seconds);

if (duration.inDays > 3) {
return '${duration.inDays}d';
}

if (duration.inHours > 3) {
return '${duration.inHours}h';
}

if (duration.inMinutes > 3) {
return '${duration.inMinutes}m';
}

return '${duration.inSeconds}s';
}
65 changes: 65 additions & 0 deletions lib/utils/time.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
extension DateTimeExtension on DateTime {
/// [toRFC3339Nano] formates the given [DateTime] to a RFC3339Nano string, which
/// is used to set the `reconcile.fluxcd.io/requestedAt` annotation.
String toRFC3339Nano() {
final t = toUtc();

String y = (t.year >= -9999 && t.year <= 9999)
? _fourDigits(t.year)
: _sixDigits(t.year);
String m = _twoDigits(t.month);
String d = _twoDigits(t.day);
String h = _twoDigits(t.hour);
String min = _twoDigits(t.minute);
String sec = _twoDigits(t.second);
String ms = _threeDigits(t.millisecond);
String us = t.microsecond == 0 ? '' : _threeDigits(t.microsecond);

return '$y-$m-${d}T$h:$min:$sec.$ms$us+00:00';
}

/// [toRFC3339] formates the given [DateTime] to a RFC3339 string, which is
/// used to set a time as used by Kubernetes.
String toRFC3339() {
final t = toUtc();

String y = (t.year >= -9999 && t.year <= 9999)
? _fourDigits(t.year)
: _sixDigits(t.year);
String m = _twoDigits(t.month);
String d = _twoDigits(t.day);
String h = _twoDigits(t.hour);
String min = _twoDigits(t.minute);
String sec = _twoDigits(t.second);

return '$y-$m-${d}T$h:$min:${sec}Z';
}
}

String _fourDigits(int n) {
int absN = n.abs();
String sign = n < 0 ? '-' : '';
if (absN >= 1000) return '$n';
if (absN >= 100) return '${sign}0$absN';
if (absN >= 10) return '${sign}00$absN';
return '${sign}000$absN';
}

String _sixDigits(int n) {
assert(n < -9999 || n > 9999);
int absN = n.abs();
String sign = n < 0 ? '-' : '+';
if (absN >= 100000) return '$sign$absN';
return '${sign}0$absN';
}

String _threeDigits(int n) {
if (n >= 100) return '$n';
if (n >= 10) return '0$n';
return '00$n';
}

String _twoDigits(int n) {
if (n >= 10) return '$n';
return '0$n';
}
50 changes: 2 additions & 48 deletions lib/widgets/plugins/flux/actions/plugin_flux_reconcile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:kubenav/services/kubernetes_service.dart';
import 'package:kubenav/utils/constants.dart';
import 'package:kubenav/utils/logger.dart';
import 'package:kubenav/utils/showmodal.dart';
import 'package:kubenav/utils/time.dart';
import 'package:kubenav/widgets/resources/resources/resources.dart';
import 'package:kubenav/widgets/shared/app_bottom_sheet_widget.dart';

Expand Down Expand Up @@ -75,7 +76,7 @@ class _PluginFluxReconcileState extends State<PluginFluxReconcile> {
}

final item = await compute(widget.resource.toJson, widget.item);
final now = _rFC3339Nano(DateTime.now());
final now = DateTime.now().toRFC3339Nano();

final String body = item['metadata'] != null &&
item['metadata']['annotations'] != null &&
Expand Down Expand Up @@ -159,50 +160,3 @@ class _PluginFluxReconcileState extends State<PluginFluxReconcile> {
);
}
}

/// [_rFC3339Nano] formates the given [DateTime] to a RFC3339Nano string, which
/// is used to set the `reconcile.fluxcd.io/requestedAt` annotation.
String _rFC3339Nano(DateTime time) {
final t = time.toUtc();

String y = (t.year >= -9999 && t.year <= 9999)
? _fourDigits(t.year)
: _sixDigits(t.year);
String m = _twoDigits(t.month);
String d = _twoDigits(t.day);
String h = _twoDigits(t.hour);
String min = _twoDigits(t.minute);
String sec = _twoDigits(t.second);
String ms = _threeDigits(t.millisecond);
String us = t.microsecond == 0 ? '' : _threeDigits(t.microsecond);

return '$y-$m-${d}T$h:$min:$sec.$ms$us+00:00';
}

String _fourDigits(int n) {
int absN = n.abs();
String sign = n < 0 ? '-' : '';
if (absN >= 1000) return '$n';
if (absN >= 100) return '${sign}0$absN';
if (absN >= 10) return '${sign}00$absN';
return '${sign}000$absN';
}

String _sixDigits(int n) {
assert(n < -9999 || n > 9999);
int absN = n.abs();
String sign = n < 0 ? '-' : '+';
if (absN >= 100000) return '$sign$absN';
return '${sign}0$absN';
}

String _threeDigits(int n) {
if (n >= 100) return '$n';
if (n >= 10) return '0$n';
return '00$n';
}

String _twoDigits(int n) {
if (n >= 10) return '$n';
return '0$n';
}
125 changes: 125 additions & 0 deletions lib/widgets/resources/actions/csr_approve.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'package:kubenav/models/kubernetes/io_k8s_api_certificates_v1_certificate_signing_request.dart';
import 'package:kubenav/repositories/app_repository.dart';
import 'package:kubenav/repositories/clusters_repository.dart';
import 'package:kubenav/services/kubernetes_service.dart';
import 'package:kubenav/utils/constants.dart';
import 'package:kubenav/utils/logger.dart';
import 'package:kubenav/utils/showmodal.dart';
import 'package:kubenav/utils/time.dart';
import 'package:kubenav/widgets/resources/resources/resources.dart';
import 'package:kubenav/widgets/shared/app_bottom_sheet_widget.dart';

class CSRApprove extends StatefulWidget {
const CSRApprove({
super.key,
required this.name,
required this.csr,
required this.resource,
});

final String name;
final IoK8sApiCertificatesV1CertificateSigningRequest csr;
final Resource resource;

@override
State<CSRApprove> createState() => _CSRApproveState();
}

class _CSRApproveState extends State<CSRApprove> {
bool _isLoading = false;

Future<void> _approve() async {
ClustersRepository clustersRepository = Provider.of<ClustersRepository>(
context,
listen: false,
);
AppRepository appRepository = Provider.of<AppRepository>(
context,
listen: false,
);

try {
setState(() {
_isLoading = true;
});

final now = DateTime.now().toRFC3339();
final String body =
'[{"op":"add","path":"/status/conditions","value":[{"type":"Approved","status":"True","reason":"KubenavApprove","message":"This CSR was approved by kubenav certificate approve.","lastUpdateTime":"$now","lastTransitionTime":null}]}]';

final cluster = await clustersRepository.getClusterWithCredentials(
clustersRepository.activeClusterId,
);
final url =
'${widget.resource.path}/${widget.resource.resource}/${widget.name}/approval';

await KubernetesService(
cluster: cluster!,
proxy: appRepository.settings.proxy,
timeout: appRepository.settings.timeout,
).patchRequest(url, body);

setState(() {
_isLoading = false;
});
if (mounted) {
showSnackbar(
context,
'${widget.resource.singular} Approved',
'The ${widget.resource.singular} ${widget.name} was approved',
);
Navigator.pop(context);
}
} catch (err) {
Logger.log(
'CSRApprove _approve',
'Failed to Approve ${widget.resource.singular}',
err,
);
setState(() {
_isLoading = false;
});
if (mounted) {
showSnackbar(
context,
'Failed to Approve ${widget.resource.singular}',
err.toString(),
);
}
}
}

@override
Widget build(BuildContext context) {
return AppBottomSheetWidget(
title: 'Approve',
subtitle: widget.name,
icon: Icons.task_alt,
closePressed: () {
Navigator.pop(context);
},
actionText: 'Approve',
actionPressed: () {
_approve();
},
actionIsLoading: _isLoading,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
top: Constants.spacingMiddle,
bottom: Constants.spacingMiddle,
left: Constants.spacingMiddle,
right: Constants.spacingMiddle,
),
child: Text(
'Do you really want to approve the ${widget.resource.singular} ${widget.name}?',
),
),
),
);
}
}
Loading

0 comments on commit eab8cb5

Please sign in to comment.